A framework is the shared rulebook every script on your server agrees to. Get the player, read their job, move their money, check their inventory — without one, every resource reinvents those wheels and none of them talk to each other. This lesson explains what frameworks hand you, how the three big ones differ, and why the single most useful thing you can tell an AI is which one you're on.
What a framework actually gives you
When someone connects to your server, raw FiveM knows almost nothing about them. It has a player ID and a game character. It does not know they're a cop, that they have $4,000 in the bank, or that they're carrying a lockpick.
A framework fills that gap. It wraps every player in a player object — a structured bundle of data plus functions to change it safely. From that object you get four things you'll use constantly:
- Identity — a stable identifier that survives reconnects, so data persists in your database.
- Job — the player's role and rank, often with an on/off duty flag. This is how you gate who can do what.
- Money — separate balances (cash, bank, sometimes dirty money) with functions that add, remove, and sync them.
- Inventory — what they're carrying. On a modern build this is delegated to ox_inventory, a standalone resource the framework hooks into.
The point of the framework is that it owns this state and keeps it consistent. You don't edit a money field by hand; you call a function that updates the value, syncs it to the client's HUD, and writes it to the database in one motion.
ESX vs QBCore vs Qbox — the honest version
ESX is the old guard. It's everywhere, there are thousands of scripts for it, and that's exactly why it carries baggage — years of mixed conventions, deprecated patterns, and tutorials that still teach 2019-era code. It works, but it's the least clean place to start fresh.
QBCore came later and is more roleplay-shaped out of the box: metadata, richer job structures, a tighter ecosystem. It's the most popular base for new RP servers today.
Qbox is a community fork of QBCore, rebuilt around the ox stack (ox_lib, ox_inventory, ox_target). It strips legacy cruft, leans on standalone libraries instead of bundling everything, and exposes a cleaner, more consistent API. If you're starting in 2026, this is the modern lane. Qbox and QBCore are close cousins — skills transfer directly — so we teach Qbox/QBCore patterns here and treat ESX as legacy you'll only touch when a script demands it.
The throughline: lean modern. Qbox or QBCore, with ox_lib for callbacks and UI, ox_inventory for items, ox_target for interaction. That's the stack the rest of Track A assumes.
Pick one. Pin it. Tell your AI.
Here's the failure mode that wastes more beginner hours than any other: mixing frameworks.
AI is fluent in all three, which sounds great until it isn't. Ask for a "give money" snippet and it'll cheerfully blend ESX.GetPlayerFromId, QBCore.Functions.GetPlayer, and a Qbox export into one file that calls three functions, two of which don't exist on your server. It also invents natives and exports that sound plausible but were never real. None of it errors the way you'd hope — it just silently does nothing, or crashes deep in another resource.
The fix is boring and total: commit to one framework and pin your AI to it. Start every prompt with the context — "I'm on Qbox using ox_lib and ox_inventory" — and tell it explicitly not to use ESX or QBCore syntax. Then verify what it gives you against two sources: the framework's official documentation (link to it, never trust a snippet's word that an export exists) and the PlayDeck sandbox, where you can run the logic and watch it behave before it ever touches a server.
Getting the core object and reading player data
On Qbox you reach a player through an export, then read fields off the object it returns. Here's a complete, working example — a shift bonus that pays only on-duty mechanics, with every check on the server.
-- server/payout.lua
-- Pays a bonus only to on-duty mechanics. Every decision runs server-side.
-- We use lib.callback (ox_lib), the modern, framework-agnostic callback system —
-- not the old per-framework callbacks you'll see in stale tutorials.
lib.callback.register('garage:claimShiftBonus', function(source)
-- Fetch the player object fresh from the framework core by source.
-- On Qbox this is an EXPORT, not a global. Don't cache it long-term.
local player = exports.qbx_core:GetPlayer(source)
if not player then return false end
local job = player.PlayerData.job -- read job data off the player object
-- Business rule: mechanics, clocked in, only. Both checks live here because
-- the client can lie about its job and its duty status.
if job.name ~= 'mechanic' or not job.onduty then
return false
end
local bonus = 250 -- flat shift bonus, in dollars; tune to your economy
-- Move money through the framework API so the HUD syncs and the DB persists.
-- Editing a balance field by hand would desync the player and lose on restart.
player.Functions.AddMoney('bank', bonus, 'mechanic-shift-bonus')
return bonus
end)
-- client/payout.lua
-- The client REQUESTS. It never decides. The return value is just for the UI.
RegisterCommand('shiftbonus', function()
local paid = lib.callback.await('garage:claimShiftBonus', false)
if paid then
lib.notify({ description = ('+$%d to your bank'):format(paid), type = 'success' })
else
lib.notify({ description = 'You must be an on-duty mechanic.', type = 'error' })
end
end)
Never trust the client
Read the example again and notice where the job check lives. It's on the server. That's not a style choice — it's the security model of every multiplayer game.
The client is the player's machine, and a determined player can make it send any event with any payload. If you check "are they a mechanic?" on the client and let the server pay out on faith, you've built a money printer. AI loves to write this bug, because client-side logic reads cleaner in a demo. Anything that grants money, items, or permissions is decided on the server, full stop. The client asks; the server validates and acts.
Practice
No FiveM needed. In the PlayDeck browser sandbox, paste this and run it:
-- A fake "player object" like the framework would hand you.
local player = { PlayerData = { job = { name = 'mechanic', onduty = true } } }
local function canClaimBonus(p)
local job = p.PlayerData.job
return job.name == 'mechanic' and job.onduty
end
print('Can claim:', canClaimBonus(player))
Now flip onduty to false and run again. Then change name to 'police'. Watch the gate reject both. You just wrote the exact server-side check the payout depends on — proven before any server touched it.
Recap
- A framework gives you the player object: identity, job, money, and inventory APIs that stay consistent.
- ESX is legacy; QBCore is the popular RP base; Qbox is the clean, ox-first modern choice. Lean Qbox/QBCore.
- Use the modern stack: ox_lib (lib.callback), ox_inventory, ox_target — not deprecated framework callbacks.
- Pin your AI to one framework. Mixing them produces fake exports and silent failures. Verify against official docs and the sandbox.
- Acquire the core object server-side, read data off it, and validate every grant on the server. The client asks; it never decides.