Most FiveM tutorials you'll find — and most code an AI hands you — are stuck in the ESX era of 2019. Modern roleplay servers run on the ox suite: shared resources that handle UI, interactions, and inventory the same way across frameworks. Learn this stack and you'll read real scripts fluently, and you'll instantly smell when AI gives you something stale.
What "ox" actually is
"ox" is a family of open-source resources from the Overextended team. The three you'll touch constantly:
- ox_lib — a shared library: notifications, menus, progress bars, and server/client callbacks. (Licensed LGPL-3.0 — free to use, including on paid servers, but keep the license file and credit intact.)
- ox_target — the "look at a thing and press a key" interaction system. (MIT licensed — very permissive; keep the copyright notice.)
- ox_inventory — a full item, shop, and stash system that replaces your framework's built-in inventory.
These work with Qbox (the modern fork) and QBCore, and they're framework-agnostic by design. One rule that will save you hours: pin ONE framework and stick to it. AI loves to blend ESX, QBCore, and Qbox in the same file — ESX.GetPlayerData() sitting next to QBCore.Functions.GetPlayer(). That never runs. Pick Qbox or QBCore, and reject any snippet that mixes them.
ox_lib: UI and callbacks
ox_lib gives you four things you'll use in almost every script:
lib.notify— toast messages.lib.registerContext/lib.showContext— clickable menus.lib.progressBar— the "doing a thing" bar with a duration.lib.callback— ask the server a question and wait for the answer.
That last one matters most. Old ESX tutorials use TriggerServerCallback with a magic string; AI still emits it constantly. The modern pattern is lib.callback.register on the server and lib.callback.await on the client. Same idea — client asks, server decides — but it's the current API. If a snippet uses the old callback style, that's a tell that the model is working from outdated training data.
ox_target: interactions
ox_target lets a player aim at an entity, model, zone, or coordinate and pick an option. You register options with exports like addModel, addEntity, or addBoxZone. Each option has a label, an icon, and an onSelect function.
Here's the trap, and it's the single most important idea in this module: onSelect runs on the client, and the client is the attacker. Anyone can trigger it with a fake menu or a Lua executor. So onSelect must never grant rewards directly. It should ask the server, and the server validates.
A complete, secure example
A repair bench. The player targets a workbench prop, we check server-side that they own a repair kit, remove it, then show progress.
-- client.lua — runs on the player's machine. Treat all of this as untrusted.
exports.ox_target:addModel(`prop_tool_bench02`, {
{
name = 'pd_repair_phone',
icon = 'fa-solid fa-wrench',
label = 'Repair Phone',
onSelect = function()
-- await pauses until the SERVER replies true/false.
-- The client never decides whether this is allowed.
local allowed = lib.callback.await('pd_repair:start', false)
if not allowed then return end
-- The kit was already consumed server-side. This bar is cosmetic.
if lib.progressBar({
duration = 4000,
label = 'Repairing...',
canCancel = true,
disable = { move = true, combat = true },
}) then
lib.notify({ type = 'success', description = 'Phone repaired.' })
end
end,
},
})
-- server.lua — the source of truth. Registered once, owned by the server.
lib.callback.register('pd_repair:start', function(source)
-- Ask ox_inventory how many kits THIS player really has.
-- A hacked client cannot lie to a server-side count.
local kits = exports.ox_inventory:Search(source, 'count', 'repair_kit')
if kits < 1 then
TriggerClientEvent('ox_lib:notify', source, {
type = 'error',
description = 'You need a repair kit.',
})
return false
end
-- Remove BEFORE returning success. RemoveItem returns false if it couldn't.
if not exports.ox_inventory:RemoveItem(source, 'repair_kit', 1) then
return false
end
return true
end)
Notice the export names: ox_target:addModel, ox_inventory:Search, ox_inventory:RemoveItem. Verify every export signature against the official ox docs — not against my comments, and definitely not against an AI's memory. The number one AI failure in FiveM is inventing exports that don't exist (ox_inventory:GiveItemToPlayer — not a real export). When in doubt, the real docs and the PlayDeck sandbox are your truth.
ox_inventory: items, shops, stashes
ox_inventory owns three concepts:
- Items — defined in
data/items.luawith a name, label, weight, and optional metadata. - Shops — registered NPC stores that sell items for money.
- Stashes — persistent storage containers (a trunk, a locker, a stash behind a counter).
The same security rule applies: register and validate everything server-side. Client code can request that a stash open; the server enforces who's allowed and what's inside.
Resource start order
ox resources depend on each other, so server.cfg load order matters. A clean order:
ensure ox_lib
ensure oxmysql
ensure qbx_core # your ONE framework
ensure ox_inventory
ensure ox_target
ensure your_resource
ox_lib and your database layer load first because everything leans on them. Your own resource loads last. Also declare dependencies in your fxmanifest.lua so the server warns you when something's missing. If a script "can't find export," wrong start order is the usual culprit — AI rarely mentions this, because load order isn't in the code it copied.
Practice
No FiveM needed — run this in the PlayDeck browser sandbox. The point is the server-side validation logic, stripped of the game:
-- A pure-Lua model of the server check above.
local function tryRepair(inventory)
local kits = inventory.repair_kit or 0
if kits < 1 then
return false, "no kit"
end
inventory.repair_kit = kits - 1
return true, "repaired"
end
local bag = { repair_kit = 1 }
print(tryRepair(bag)) -- true repaired
print(tryRepair(bag)) -- false no kit (the kit is gone)
Run it. Then change the starting bag to { repair_kit = 3 } and call tryRepair four times. Watch it succeed three times, then fail — exactly how the server protects your economy.
Recap
- The modern stack is ox_lib + ox_target + ox_inventory, on Qbox or QBCore — pick one, never mix frameworks.
- Use
lib.callback, not legacy ESX callbacks. Old callback syntax = outdated AI output. onSelectruns on the client; the client is the attacker. Always validate and mutate state server-side.- ox_inventory owns items, shops, and stashes — register and check everything on the server.
- Mind the start order: ox_lib and oxmysql first, framework next, then ox_inventory and ox_target, then your resource.
- Verify every export against the official ox docs and the sandbox. AI invents natives and exports — trust the reference, not the autocomplete.