Almost every "it works on my machine but players exploit it" bug comes from one misunderstanding: not knowing what runs where, and who controls each side. This lesson fixes that. Get the client/server split right and you'll write scripts that are both functional and cheat-resistant — and you'll catch the #1 mistake AI makes when it writes FiveM code.
What a native actually is
A native is a function built into the game engine (and the FiveM runtime) that you call from your script. You didn't write it; Rockstar and the CitizenFX team did. Spawning a vehicle, getting a player's coordinates, drawing text on screen, freezing a ped — each is a native you invoke by name.
Think of natives as the verbs your script is allowed to use. Lua gives you if, for, and tables. Natives give you GetEntityCoords, CreateVehicle, SetEntityHealth. Without them your Lua can't touch the game world at all.
Two things matter about every native: where it runs (client, server, or shared) and what it actually does. You confirm both by reading the reference — never by guessing.
Reading the native reference (and why you link, never memorize)
The official native reference lives at https://docs.fivem.net/natives/. Bookmark it. Each entry tells you the namespace, the parameters, the return value, and — critically — a context tag marking it client, server, or shared.
Here's the discipline: look it up every time. Don't trust your memory, and absolutely don't trust an AI's memory. When you ask an AI to write a script, it will confidently produce natives that look real but don't exist, or it will call a client-only native on the server. The reference is the source of truth; the AI is a fast first draft that you verify against it.
In PlayDeck we never copy the reference into the lesson — it changes, and copying it teaches you to memorize instead of look up. Open it in a tab while you code.
Two runtimes: client vs server
A FiveM resource runs in two completely separate places at once:
- Client — runs on each player's PC. It can see and manipulate the game world: peds, vehicles, the camera, the UI, key presses. The world only exists on clients, so anything visual or physical is a client native.
- Server — runs on the machine hosting your server. It has no game world. It can't draw text or read coordinates directly. What it can do is hold authority: player identifiers, money, inventory, the database, and the rules everyone must obey.
Why the split? Because the world is rendered per-player, but the truth — who owns what, who has how much cash — must live in one trusted place. If every client decided its own bank balance, the richest player would be whoever cheats hardest.
The two sides talk through events and callbacks. In the modern ox stack you use lib.callback from ox_lib instead of the old TriggerCallback patterns. The client asks the server a question; the server answers — on its own terms.
The trust boundary: the client is the attacker
This is the most important sentence in the module: the client is fully controlled by the player, and some players are cheating.
A player can edit their client files, inject tools, and send your server any event with any data they like. So every value that arrives from a client is a claim, not a fact. "I sold 10 fish for $50 each" is something the client says, not something that happened.
The rule that follows: never trust the client. Validate on the server. The server checks the inventory it controls, calculates the payout from prices it controls, and only then moves money. The client's job is to ask; the server's job is to decide.
This is exactly where AI-generated scripts fail. They take the amount and price straight from the client and pay out — a free-money exploit. AI also loves to mix frameworks (ESX money calls in a QBCore script) and invent exports. Your defenses: pin ONE framework (we use Qbox/QBCore here — pick one and stay), verify every native and export against the reference and that framework's docs, and always do the security-critical math server-side.
A worked example: selling fish, the safe way
-- CLIENT: client/sell.lua
-- The client only signals intent. It sends NO price and NO trusted amount.
RegisterCommand('sellfish', function()
-- Ask the server to handle the sale. The 'false' means we don't need
-- to block on a return value here. (Confirm lib.callback's signature
-- in the ox_lib docs — never assume an export from memory.)
lib.callback('playdeck:sellFish', false, function(result)
lib.notify({ description = result.message })
end)
end)
-- SERVER: server/sell.lua
local PRICE_PER_FISH = 12 -- price lives on the SERVER, never the client
lib.callback.register('playdeck:sellFish', function(source)
-- 'source' is the server-assigned player id. The server, not the client,
-- decides whose inventory to read — a spoofed id can't redirect this.
local count = exports.ox_inventory:Search(source, 'count', 'fish')
if not count or count <= 0 then
return { message = 'You have no fish to sell.' }
end
-- Remove first. If removal fails, no payout happens — money is never
-- created from an item the player didn't actually own.
if not exports.ox_inventory:RemoveItem(source, 'fish', count) then
return { message = 'Sale failed, try again.' }
end
local payout = count * PRICE_PER_FISH -- computed from trusted values only
local player = exports.qbx_core:GetPlayer(source)
player.Functions.AddMoney('cash', payout, 'sold-fish')
return { message = ('Sold %d fish for $%d.'):format(count, payout) }
end)
Notice the client sends nothing worth faking — no count, no price. The server reads the inventory it owns, sets the price it owns, and pays from that. There's no number for a cheater to inflate.
Practice
Run this in the PlayDeck browser sandbox — no FiveM needed. It models the trust boundary in pure Lua:
-- A naive server that TRUSTS the client's claim:
local function naiveSell(clientClaim)
return clientClaim.count * clientClaim.price -- exploitable!
end
-- A cheating client:
local fakeRequest = { count = 9999, price = 500 }
print('Naive payout:', naiveSell(fakeRequest)) -- prints a fortune
-- Your task: write safeSell(source) that ignores clientClaim entirely.
-- Use a fixed PRICE = 12 and a hard-coded inventory of 3 fish.
-- It should always pay 36, no matter what the client sends.
Write safeSell so the cheater's fakeRequest can't change the result. When the payout stays 36, you understand the boundary.
Recap
- A native is an engine function your script calls; confirm what it does and where it runs in the reference at https://docs.fivem.net/natives/ — link it, don't memorize it.
- Resources run on client (the game world, per player) and server (the trusted authority). They talk via events and
lib.callback. - The client is attacker-controlled. Treat its data as claims and validate on the server.
- AI invents natives, mixes frameworks, and trusts the client. Defend by pinning one framework, verifying against the reference and the sandbox, and keeping security-critical logic server-side.