FiveM & QBCore · Practical guide

QBCore Clothing Script — Design, Secure Storage, and Integration Guide

A practical guide to building a server-authoritative QBCore clothing/outfit script: planning, file layout, server validation, client hooks, MySQL storage, and safe integration with.

Stellar AI · Updated 8 September 2026 · 5 min read

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

StepAction
Verify appearance providerConfirm exports exist to GetCurrentSkin and ApplySkin
DatabaseCreate outfits table and test read/write
Server validationTest name length, ownership, and max outfits
Client UIProvide a safe input method for outfit names
PermissionsEnsure 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.

Build your next system with Stellar AI

Describe one feature, get organized project files, then bring back your errors to keep improving. Start free with no card required. Test generated code in a private development environment before release.

Create your first script free →