This guide walks a builder through creating a robust, server-authoritative clothing (outfit) script for QBCore-based FiveM servers. It focuses on safe server validation, data storage, and clean client/server APIs so outfits are saved, listed, and applied securely and compatibly with common appearance resources.
Creating a QBCore clothing script that players trust requires a small set of design decisions: keep authority on the server, validate inputs, store outfits safely in MySQL, and integrate with the existing appearance/skinchanger on the client. This guide explains a pragmatic plan, gives complete destination-labelled files you can drop into a resource, and shows the security and validation you must include.
Implementation plan (short)
- 1) Decide the appearance provider on your server (fivem-appearance, skinchanger, custom). This guide assumes an appearance export that returns a serializable outfit table.
- 2) Add a small resource with fxmanifest.lua, config.lua, server.lua and client.lua that registers a QBCore callback and a save/apply event.
- 3) Create a MySQL table for outfits and use MySQL.Async to persist outfits server-side tied to identifiers (cid or identifier).
- 4) Validate names, ownership, and limit counts on the server. Keep all enforcement server-side.
- 5) Provide a simple client menu/command to call the save and apply flows, delegating actual ped changes to the appearance provider.
Assumptions and dependencies
- QBCore 2.x style core: local QBCore = exports['qb-core']:GetCoreObject().
- MySQL.Async (mysql-async or oxmysql) available on the server. Adjust queries to your DB layer.
- An appearance resource exposes a safe export like exports['fivem-appearance']:GetCurrentSkin() and an ApplySkin(skin) function. If your server uses a different skinchanger, adapt accordingly.
- Optional: qb-target, qb-menu or ox_lib for nice client UI; the guide uses a command for simplicity.
Destination‑labelled files
Drop these files into a resource folder named qb-clothing (or similar). Update fxmanifest if you change names.
-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
author 'YourName'
description 'QBCore clothing script (save/apply outfits)'
version '1.0.0'
shared_script 'config.lua'
server_scripts {
'@mysql-async/lib/MySQL.lua',
'server.lua'
}
client_scripts {
'client.lua'
}
-- config.lua
Config = {}
Config.MaxOutfits = 10 -- per player
Config.MinNameLength = 3
Config.MaxNameLength = 32
-- server.lua
local QBCore = exports['qb-core']:GetCoreObject()
QBCore.Functions.CreateCallback('qb-clothing:getOutfits', function(source, cb)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then cb({}) return end
local cid = Player.PlayerData.citizenid
MySQL.Async.fetchAll('SELECT id, name, outfit FROM outfits WHERE cid = @cid', {['@cid'] = cid}, function(result)
local out = {}
for _, row in ipairs(result) do
table.insert(out, {id = row.id, name = row.name, outfit = json.decode(row.outfit)})
end
cb(out)
end)
end)
RegisterNetEvent('qb-clothing:saveOutfit', function(payload)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local cid = Player.PlayerData.citizenid
local name = tostring(payload.name or '')
local outfit = payload.outfit
-- server-side validation
if #name < Config.MinNameLength or #name > Config.MaxNameLength then
TriggerClientEvent('QBCore:Notify', src, 'Invalid outfit name', 'error')
return
end
if type(outfit) ~= 'table' then
TriggerClientEvent('QBCore:Notify', src, 'Invalid outfit data', 'error')
return
end
-- enforce max outfits per player
local count = MySQL.Sync.fetchScalar('SELECT COUNT(*) FROM outfits WHERE cid = @cid', {['@cid'] = cid})
if count and tonumber(count) >= Config.MaxOutfits then
TriggerClientEvent('QBCore:Notify', src, 'Outfit limit reached', 'error')
return
end
MySQL.Async.execute('INSERT INTO outfits (cid, name, outfit) VALUES (@cid, @name, @outfit)', {
['@cid'] = cid,
['@name'] = name,
['@outfit'] = json.encode(outfit)
}, function(rows)
TriggerClientEvent('QBCore:Notify', src, 'Outfit saved', 'success')
end)
end)
RegisterNetEvent('qb-clothing:applyOutfit', function(outfitData)
local src = source
-- ensure player owns the outfit server-side
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local cid = Player.PlayerData.citizenid
local row = MySQL.Sync.fetchAll('SELECT outfit FROM outfits WHERE id = @id AND cid = @cid', {['@id'] = outfitData.id, ['@cid'] = cid})
if not row or #row == 0 then
TriggerClientEvent('QBCore:Notify', src, 'Outfit not found', 'error')
return
end
local outfit = json.decode(row[1].outfit)
-- send outfit to client to apply via the appearance provider
TriggerClientEvent('qb-clothing:receiveApplyOutfit', src, outfit)
end)
-- client.lua (simplified))
RegisterCommand('saveoutfit', function()
local name = exports['qb-input']:ShowInput({--[[grab name via UI]]});
-- ASSUMPTION: your appearance resource provides a serializable outfit
local outfit = exports['fivem-appearance']:GetCurrentSkin()
if not outfit then
QBCore.Functions.Notify('Cannot read current skin', 'error')
return
end
TriggerServerEvent('qb-clothing:saveOutfit', {name = name, outfit = outfit})
end)
RegisterNetEvent('qb-clothing:receiveApplyOutfit', function(outfit)
-- ASSUMPTION: appearance provider can apply a full outfit
exports['fivem-appearance']:ApplySkin(outfit)
end)
Database schema
Create a simple table to persist outfits. If you use oxmysql, keep syntax compatible.
CREATE TABLE `outfits` (
`id` int NOT NULL AUTO_INCREMENT,
`cid` varchar(64) NOT NULL,
`name` varchar(64) NOT NULL,
`outfit` text NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Checklist before deployment
| Step | Action |
|---|---|
| Verify appearance provider | Confirm exports exist to GetCurrentSkin and ApplySkin |
| Database | Create outfits table and test read/write |
| Server validation | Test name length, ownership, and max outfits |
| Client UI | Provide a safe input method for outfit names |
| Permissions | Ensure only authenticated players call save/apply events |
Security and validation notes
- Never apply trust to client-side data. The server must verify the caller's identity via QBCore.GetPlayer and tie outfits to a persistent identifier (citizenid or steam hex).
- Validate name length and character set server-side to prevent SQL or storage abuse.
- Enforce limits (max outfits) on the server, not the client.
- When sending outfit JSON to the client to apply, keep the payload minimal and avoid attaching arbitrary metadata.
- If your appearance provider performs sensitive model swaps, prefer to call its server-side checks when possible or sign payloads if you build a custom provider.
Integration tips
For production usability, integrate a UI: a save dialog with a preview, and an outfits menu showing thumbnails. Use qb-target on wardrobe props for a natural interaction. If you want automated backups or cross-server syncs, keep any sync authority server-side and rate-limit writes.
Want a visual workflow or to generate full file trees and alternate providers quickly? Use the Stellar AI workspace for iterative code generation and export-ready files at https://trystellarai.com/app — it helps describe system inputs and outputs, plan files, and produce destination-labelled files for your resource. If you prefer to read implementation patterns and tips first, see the Stellar AI blog for related server patterns: Stellar AI blog. When experimenting, you can also jump directly into the generator at https://trystellarai.com/app.
Next steps
- Add thumbnails: generate small images server-side or store lookups for UI previews.
- Implement rename/delete events with server-side checks and cascading UI updates.
- Hook into job/wardrobe permissions: only certain jobs can access particular wardrobe props.
Follow the listed checklist, test with multiple players, and keep enforcement on the server. This structure keeps your outfit system robust and compatible with common QBCore servers and appearance providers.