PlayDeck
Home / Course / The Modern ox Stack
Track A · Module 10

The Modern ox Stack

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:

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:

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:

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

Learn it by building it

PlayDeck is an original course on building GTA roleplay scripts with AI — Lua, frameworks, NUI, debugging, and a browser sandbox to test every lesson without booting the game.

Browse the course