01. Qbox Basics

Plain English

A framework in FiveM is the resource that owns “what is a player”. It defines the player object, jobs, gangs, money, inventory hooks, character creation, and the events that fire on login/logout.

Think of it as the server’s operating layer for gameplay logic: your scripts plug into the framework instead of reinventing player/account/job systems every time.

Qbox (qbx_core) is the most active framework in the FiveM scene right now. It’s a modern fork of QBCore - same shape, same concepts, slightly cleaner API.

What Makes Qbox Different

  • It is a modern fork of QBCore with cleaner internals and active maintenance.
  • It keeps a compatibility bridge for many QBCore-style APIs/events, so older resources usually need fewer rewrites.
  • It is commonly paired with the ox ecosystem (ox_lib, ox_inventory, ox_target) in newer stacks.

Who Made It

Qbox is built and maintained by the Qbox Project community/team.

Qbox came from the QBCore ecosystem, so you’ll still see shared patterns and names.

This lesson assumes the latest version of Qbox.


Get The Player Object (Server Side)

-- ↓ on the server, get the framework's player object for a given source
local player = exports.qbx_core:GetPlayer(src)

-- ↓ ALWAYS check it's not nil. Player can be nil if they're still connecting/loading.
if not player then return end

-- ↓ player.PlayerData = the full data blob for this player
print(player.PlayerData.citizenid)               -- unique per-character ID, like "ABC12345"
print(player.PlayerData.license)                 -- per-player ID across all characters
print(player.PlayerData.name)                    -- character display name
print(player.PlayerData.charinfo.firstname)
print(player.PlayerData.charinfo.lastname)
print(player.PlayerData.money.cash)              -- cash in pocket
print(player.PlayerData.money.bank)              -- bank account
print(player.PlayerData.job.name)                -- current job: 'police', 'unemployed', etc.
print(player.PlayerData.job.grade.level)         -- grade rank: 0, 1, 2, ...
print(player.PlayerData.job.isboss)              -- true if this is a boss-level grade
print(player.PlayerData.gang.name)               -- gang affiliation
print(player.PlayerData.metadata.hunger)         -- custom stats stored as JSON

Money Functions

Money operations are atomic - Qbox handles internal locks so concurrent calls don’t dupe.

local player = exports.qbx_core:GetPlayer(src)

-- ↓ ADD money. account type ('cash' or 'bank'), amount, reason string for logs
player.Functions.AddMoney('cash', 100, 'reason string')
player.Functions.AddMoney('bank', 500, 'salary')

-- ↓ REMOVE money. RETURNS BOOLEAN: true if had enough, false if short.
-- ALWAYS check the return value.
local ok = player.Functions.RemoveMoney('cash', 80, 'buy_gun')
if not ok then
    -- player didn't have $80, abort
    return
end

-- ↓ READ money. two equivalent ways:
local cash = player.PlayerData.money.cash        -- direct access
local cash2 = player.Functions.GetMoney('cash')  -- via function

-- ↓ SET money to a specific value. Rare. Usually use Add/Remove.
player.Functions.SetMoney('cash', 1000, 'admin set')

Reason strings matter - they go to logs. Be specific: 'shop_buy_gun', 'police_salary_grade2', 'admin_giveall'. Future-you will thank current-you.


Job Functions

-- ↓ change a player's job (e.g., they got hired)
player.Functions.SetJob('police', 2)             -- name, grade level

-- ↓ toggle on-duty / off-duty (police, EMS, etc.)
player.Functions.SetJobDuty(true)                -- true = on duty

-- reading job state was shown above via PlayerData

Get Player Data (Client Side)

The client has a synced copy of its own player data. It’s automatically updated when the server changes it.

local QBX = exports.qbx_core
local pdata = QBX:GetPlayerData()

print(pdata.citizenid)
print(pdata.money.cash)                          -- WARNING: this is for display only
print(pdata.job.name)

Don’t trust client PlayerData for security decisions. Players can edit client Lua to fake values. Only the server’s PlayerData is authoritative.


Client Events: React To Data Changes

Qbox fires events when player data changes:

-- ↓ fires when the player's job changes
RegisterNetEvent('QBCore:Client:OnJobUpdate', function(job)
    print('job changed to:', job.name)
    -- update HUD, refresh menus, etc.
end)

-- ↓ fires when the player finishes loading their character
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function()
    print('player loaded - safe to start client logic')
end)

-- ↓ fires whenever money changes. type = 'cash'/'bank', amount = delta, isRemoved = true/false
RegisterNetEvent('QBCore:Client:OnMoneyChange', function(type, amount, isRemoved)
    print(('money changed: %s %s%d'):format(type, isRemoved and '-' or '+', amount))
end)

Qbox keeps the old QBCore event names for compatibility. For “player loaded” keep using QBCore:Client:OnPlayerLoaded above - Qbox hasn’t shipped a renamed replacement.

Qbox-native events you can also listen to:

-- ↓ fires when the player logs out / disconnects
RegisterNetEvent('qbx_core:client:playerLoggedOut', function() end)

-- ↓ fires when a metadata key changes. key, previous value, new value
RegisterNetEvent('qbx_core:client:onSetMetaData', function(key, oldVal, newVal) end)

-- ↓ fires when a job/gang grade is updated
RegisterNetEvent('qbx_core:client:onGroupUpdate', function(group, grade) end)

Server Events: Player Lifecycle

-- ↓ fires when a player loads their character
AddEventHandler('QBCore:Server:OnPlayerLoaded', function(player)
    -- player object is passed in. Set up per-player state, send initial data, etc.
end)

-- ↓ fires when a player disconnects
AddEventHandler('QBCore:Server:OnPlayerUnload', function(src)
    -- cleanup per-player state, save anything you cached
end)

Job / Permission Checks

-- ↓ SERVER side check (the trusted one)
local player = exports.qbx_core:GetPlayer(src)
if not player then return end
if player.PlayerData.job.name ~= 'police' then return end       -- must be police
if player.PlayerData.job.grade.level < 2 then return end        -- must be grade 2 or higher

-- ↓ CLIENT side check (UX hint only - server is the gatekeeper)
local pdata = exports.qbx_core:GetPlayerData()
local isCop = pdata.job.name == 'police'

Repeat the lesson: client checks are UX, server checks are security.


Metadata (Custom Player State)

metadata is a free-form table stored per-player and persisted to the DB. Use it for hunger, thirst, stress, custom stats:

-- ↓ READ
local hunger = player.PlayerData.metadata.hunger or 100         -- default 100 if not set yet

--WRITE (server side)
player.Functions.SetMetaData('hunger', 80)                      -- this also auto-saves to DB

-- ↓ READ on client (synced copy)
local pdata = QBX:GetPlayerData()
print(pdata.metadata.hunger)

Anything you set with SetMetaData persists across sessions.


Identifiers

A player has multiple identifiers. Each is stable in different ways:

-- ↓ on the server, given a src
local license = GetPlayerIdentifierByType(src, 'license')       -- stable per FiveM account
local steam = GetPlayerIdentifierByType(src, 'steam')           -- only if connected via Steam
local discord = GetPlayerIdentifierByType(src, 'discord')       -- only if Discord linked
local fivem = GetPlayerIdentifierByType(src, 'fivem')           -- forum account
local ip = GetPlayerEndpoint(src)                                -- their IP:port
Identifier Stable across Use for
citizenid One character In-game things tied to a character
license All characters of one player Player-level data, ban lists
steam Steam account Optional, not all players have it
discord Discord account Optional

For ban lists, use license (survives character deletion).
For in-game money, jobs, items, use citizenid.


Get All Online Players

-- ↓ Qbox helper: returns a table indexed by source
local players = exports.qbx_core:GetQBPlayers()
for src, player in pairs(players) do
    print(src, player.PlayerData.citizenid)
end

-- ↓ alternative: use FiveM's GetPlayers() and resolve each
for _, pidStr in ipairs(GetPlayers()) do
    local p = exports.qbx_core:GetPlayer(tonumber(pidStr))
    if p then
        -- do something with p
    end
end

Get An Offline Player

-- ↓ load a player object for someone NOT currently online (admin tools, audits)
local p = exports.qbx_core:GetOfflinePlayer(citizenid)

This does a DB read. Don’t call it in a tight loop.


Notifications

Qbox doesn’t ship a notification system itself anymore - use ox_lib (covered next folder):

--from server to a specific client
TriggerClientEvent('ox_lib:notify', src, {
    title = 'Shop',
    description = 'You bought bread',
    type = 'success',                                            -- success / error / inform / warning
})

-- ↓ directly on the client
lib.notify({
    title = 'Hi',
    description = 'Hello there',
    type = 'inform',
})

Commands With Permission

Qbox registers commands through ox_lib’s lib.addCommand, not a qbx_core export:

-- ↓ name, options table, handler
lib.addCommand('adminpanel', {
    help = 'Open admin panel',
    restricted = 'group.admin',                                  -- ACE group required to run it
    params = {},                                                  -- positional args (none here)
}, function(source, args, raw)
    --source = who ran it, args = parsed params, raw = full string
    -- command body
end)

Or use FiveM’s RegisterCommand with ACE (Access Control Entries) for permissions:

--true = restricted to ACE-allowed players only
RegisterCommand('adminpanel', function(source, args)
    -- command body
end, true)
# in permissions.cfg
add_ace group.admin command.adminpanel allow

ACE is FiveM’s built-in permission system. More granular than framework groups.


Items (Via The Inventory Resource)

Qbox doesn’t store items itself. Your inventory resource does (ox_inventory, qb-inventory, tgiann-inventory, etc.). To give an item:

-- ↓ ox_inventory example. exports vary per inventory resource.
exports.ox_inventory:AddItem(src, 'bread', 5)

Each inventory resource has its own export names. Always check that resource’s docs.

Full inventory deep-dive: 06-ox-libraries/03-inventories.md.


QB Bridge (Legacy Compat)

Some older resources expect QBCore’s API:

local QBCore = exports['qb-core']:GetCoreObject()
QBCore.Functions.GetPlayer(src)

Qbox ships a bridge that maps these calls onto Qbox’s underlying functions. Old code still works on Qbox servers. For new code, prefer the native Qbox exports.


Common Mistakes

1. Mutating PlayerData directly

-- ↓ BAD: this changes the local table but doesn't persist or sync
player.PlayerData.money.cash = 500

-- ↓ GOOD: use the framework function
player.Functions.SetMoney('cash', 500, 'reason')

2. Forgetting the nil check

local player = exports.qbx_core:GetPlayer(src)
if not player then return end                                   -- ALWAYS this line. ALWAYS.

player is nil if the player just connected and hasn’t finished loading. Skipping the check = attempt to index a nil value error.

3. Trusting client PlayerData

Client PlayerData is for display. Authoritative checks happen server-side. A modded client can fake any value in client PlayerData.


TL;DR

  • exports.qbx_core:GetPlayer(src) server side - always check for nil
  • exports.qbx_core:GetPlayerData() client side - for display only
  • Money: AddMoney, RemoveMoney (returns bool!), read .money.cash / .money.bank
  • Job: .job.name, .job.grade.level, SetJob
  • Metadata: .metadata.x, SetMetaData
  • citizenid = per-character ID. license = per-player ID.
  • Notifications: ox_lib:notify event or lib.notify direct
  • Items: through your inventory resource, not the framework

Sources


Next: 05-frameworks/02-esx-basics.md