PiTyUs.Hire me

Volume 2 · Free handbook

The FiveM Developer Handbook

Everything a FiveM developer needs in one place: the language, how resources are put together, how the client and server talk, the natives and threads that drive the game, where to keep state, how to use the database and the popular frameworks, how to build NUI, how to write code that cannot be exploited and does not cost frames, how to debug it, and how to ship and sell it. Every chapter has working code and links to a detailed guide.

chapters
20
reading time
65 min
APIs & natives
33
glossary terms
34

Who this handbook is for

  • Beginners writing their first FiveM resource
  • Server owners who edit scripts and want to understand them
  • Developers moving between ESX, QBCore, Qbox and ox_core
  • Script sellers who want secure, fast, well-documented products

Updated 11 September 2026 · checked against the official Cfx.re docs

Your first resource in ten minutes

  1. 01Install VS Code with the Lua Language Server extension and a local FXServer — see local development server.
  2. 02Create resources/[local]/hello/ with an fxmanifest.lua: fx_version 'cerulean', game 'gta5', client_script 'client.lua', server_script 'server.lua'.
  3. 03In server.lua: RegisterCommand('hello', function(src) print(('hello from %s'):format(GetPlayerName(src))) end).
  4. 04In client.lua: RegisterCommand('pos', function() print(GetEntityCoords(PlayerPedId())) end).
  5. 05Run refresh and ensure hello in the server console, connect with connect localhost:30120.
  6. 06Type /hello and /pos in chat; read the output in the server console and in F8.

Chapter 01

Languages, runtimes and tooling

FiveM runs Lua, JavaScript/TypeScript and C#. Almost every public resource and every major framework is Lua, so start there and set up an editor that understands it.

RuntimeUse it forNotes
Lua (CfxLua, based on Lua 5.4)Almost everythingVectors, backtick hashes, json, promise and msgpack built in
JavaScript / TypeScriptNUI, npm libraries on the serverServer JS runs on Node.js 16 by default, or Node 22 with node_version '22'
C# (.NET)Teams already on .NETdotnet new cfx-resource templates; strong typing
  • Editor: VS Code with the Lua Language Server (LuaLS) plus Overextended’s fivem-lls-addon for native and CfxLua typings.
  • Local server: FXServer on your PC with a separate development database; sv_lan true skips the key check.
  • Version control: Git from day one, with secrets in an ignored secrets.cfg.
  • Typings for JS/TS: @citizenfx/client and @citizenfx/server.
.luarc.json — LuaLS with FiveM typings and ox_libjson
{
  "runtime.version": "Lua 5.4",
  "workspace.userThirdParty": ["C:/lua-addons"],
  "workspace.library": ["../[libs]/ox_lib"],
  "diagnostics.globals": ["lib", "cache"]
}

Chapter 02

Lua for FiveM in one chapter

You do not need all of Lua to write good FiveM scripts. You need locals, tables, loops, functions and a handful of CfxLua extras.

The parts you use every daylua
local price = 250                    -- always local
local name = 'Adder'
local owned = false

-- only nil and false are falsy: 0 and '' are true
if price > 100 and not owned then
    print(('%s costs $%d'):format(name, price))
end

-- arrays start at 1
local garages = { 'pillbox', 'legion', 'airport' }
for i = 1, #garages do print(i, garages[i]) end

-- dictionaries
local prices = { adder = 1000000, sultan = 45000 }
for model, p in pairs(prices) do print(model, p) end

-- functions are values
local function tax(amount) return math.floor(amount * 0.15) end
CfxLua extraslua
local pos = vector3(215.8, -810.1, 30.7)   -- a real vector type
local here = GetEntityCoords(PlayerPedId())
local dist = #(here - pos)                   -- distance, fast

if GetEntityModel(veh) == `adder` then end  -- compile-time hash

local data = json.decode('{"a":1}')       -- json is global

Chapter 03

Anatomy of a resource

A resource is a folder in resources/ with an fxmanifest.lua. The manifest decides which files run where, which files are sent to players and what must start first.

texttext
nc_garage/
  fxmanifest.lua
  config.lua          shared settings
  server/config.lua   server-only settings (secrets, rewards)
  client/main.lua
  server/main.lua
  locales/en.json
  web/dist/           built NUI
  sql/install.sql
  README.md
fxmanifest.lualua
fx_version 'cerulean'
game 'gta5'
lua54 'yes'

name 'nc_garage'
author 'you'
version '1.0.0'

shared_scripts { '@ox_lib/init.lua', 'config.lua' }
client_scripts { 'client/*.lua' }
server_scripts { '@oxmysql/lib/MySQL.lua', 'server/config.lua', 'server/*.lua' }

ui_page 'web/dist/index.html'
files { 'web/dist/**', 'locales/*.json' }

dependencies { 'ox_lib', 'oxmysql' }

escrow_ignore { 'config.lua', 'server/config.lua', 'locales/*.json' }
EntryMeaning
client_scriptsRun on every player’s PC — and are downloaded by them
server_scriptsRun only on the server; never sent to players
shared_scriptsRun on both sides
filesExtra files clients may load (NUI, JSON, meta)
ui_pageThe HTML page for this resource’s NUI
dependenciesResources that must be running first
escrow_ignoreFiles that stay readable when the resource is escrowed

Chapter 04

Client vs server: who does what

The single most important rule in FiveM development: the client asks, the server decides.

Client scriptsServer scripts
Drawing markers, text, UIMoney, items, jobs, permissions
Reading keys and controlsDatabase reads and writes
Animations, camera, local effectsValidating every request from clients
Zones, targets, proximity checksServer-side entity creation
Anything a cheater could changeSecrets: tokens, webhooks, API keys

A client script can be read and modified by the player who runs it, and any event it sends can be sent by a cheat menu with different values. So the client only ever sends a request (“I want to buy item X at shop Y”), and the server looks up the price, checks distance, money and cooldowns, and makes the change. Full explanation: client vs server scripts.

Chapter 05

Events, callbacks and exports

Resources and the two sides of a resource talk through events (fire and forget), callbacks (request and answer) and exports (direct function calls between resources).

Eventslua
-- server: allow the event from the network and handle it
RegisterNetEvent('garage:store', function(netId)
    local src = source
    -- validate, then act
end)

-- client -> server
TriggerServerEvent('garage:store', NetworkGetNetworkIdFromEntity(veh))

-- server -> one client, or everyone with -1
TriggerClientEvent('garage:notify', src, 'Stored')
TriggerClientEvent('garage:notify', -1, 'Garage open')
Callbacks with ox_liblua
-- server
lib.callback.register('garage:list', function(source)
    return MySQL.query.await('SELECT plate, model FROM vehicles WHERE owner = ?', { GetOwner(source) })
end)

-- client
local cars = lib.callback.await('garage:list', false)
Exportslua
-- in nc_garage
exports('IsVehicleStored', function(plate) return Stored[plate] == true end)

-- in any other resource
local stored = exports.nc_garage:IsVehicleStored('ABC 123')
UseWhen
TriggerServerEvent / TriggerClientEventOne side tells the other something happened
Callback (lib.callback, ESX/QB callbacks)One side needs an answer from the other
exportsAnother resource on the same side needs your function
AddEventHandler without RegisterNetEventEvents only other scripts on the same side may trigger

Chapter 06

Natives, threads and Wait

Natives are the game’s functions; threads are how your code runs alongside the game without freezing it.

The natives reference lists every native by namespace. In Lua and JS they are called in PascalCase: GET_ENTITY_COORDS becomes GetEntityCoords. Game natives are client-side; the CFX namespace adds server natives such as GetPlayerIdentifiers, SetPlayerRoutingBucket and CreateVehicleServerSetter. Out-parameters come back as extra return values.

An adaptive loop — fast only when it has to belua
local shop = vector3(25.7, -1347.3, 29.5)

CreateThread(function()
    while true do
        local sleep = 1000
        local dist = #(GetEntityCoords(PlayerPedId()) - shop)
        if dist < 20.0 then
            sleep = 0
            DrawMarker(1, shop.x, shop.y, shop.z - 1.0, 0, 0, 0, 0, 0, 0, 1.5, 1.5, 0.5, 124, 92, 255, 150, false, false, 2, false, nil, nil, false)
            if dist < 1.5 and IsControlJustPressed(0, 38) then
                TriggerServerEvent('shop:open')
            end
        end
        Wait(sleep)
    end
end)
  • Wait(0) runs every frame — only while drawing or reading controls.
  • Far from anything interesting, sleep for 500–1000 ms or more.
  • Better still, replace loops with ox_lib points and zones or events.
  • SetTimeout(ms, fn) runs something once after a delay.

Chapter 07

Where to keep state: state bags, convars, KVP, database

Most “it works for me but not for others” bugs are state kept in the wrong place. Pick the store by who needs to see it and how long it must live.

StoreScopeLifetimeUse for
Local Lua variable/tableOne side, one resourceUntil resource restartCaches, runtime data
State bag (GlobalState, Player(src).state, Entity(ent).state)Synced to clients that can see itUntil restart or entity deletionDuty status, vehicle fuel, door state
Convar (GetConvar)Server; setr replicates to clientsSet in server.cfgConfiguration and secrets (set only)
KVP (SetResourceKvp)Per resource, on the client PC or the serverPersistentSmall preferences and flags
Database (oxmysql)ServerPersistentAnything valuable or shared
State bagslua
-- server
Entity(veh).state:set('fuel', 64.0, true)
Player(src).state:set('onDuty', true, true)

-- client
AddStateBagChangeHandler('fuel', nil, function(bagName, key, value)
    local ent = GetEntityFromStateBagName(bagName)
    if ent ~= 0 then SetVehicleFuelLevel(ent, value) end
end)

Chapter 08

Entities, network IDs and server-side spawning

Entity handles are local numbers; network IDs are what you send between client and server. Spawning important entities on the server makes them reliable and lets you lock the client out.

server — spawn a vehicle and hand it to the playerlua
local function spawnFor(src, model, coords)
    local veh = CreateVehicleServerSetter(model, 'automobile', coords.x, coords.y, coords.z, coords.w)
    while not DoesEntityExist(veh) do Wait(0) end
    SetVehicleNumberPlateText(veh, 'NC' .. math.random(1000, 9999))
    SetPedIntoVehicle(GetPlayerPed(src), veh, -1)
    return NetworkGetNetworkIdFromEntity(veh)
end
client — turn the network ID back into an entitylua
local veh = NetworkGetEntityFromNetworkId(netId)
if DoesEntityExist(veh) then
    SetVehicleEngineOn(veh, true, true, false)
end
  • Never send entity handles over events — they mean different things on each machine.
  • Each networked entity has an owner that simulates it; others must request control to change it.
  • Delete what you create: on job end, on playerDropped and on onResourceStop.
  • Once every script spawns server-side, turn on sv_entityLockdown.

Chapter 09

The database with oxmysql

oxmysql is the standard database resource. Use its awaitable functions, always pass values as parameters, and design tables around the questions you ask.

FunctionReturns
MySQL.query.await(sql, params)All rows
MySQL.single.await(sql, params)The first row, or nil
MySQL.scalar.await(sql, params)The first column of the first row
MySQL.insert.await(sql, params)The new row’s ID
MySQL.update.await(sql, params)Number of affected rows
MySQL.prepare.await(sql, params)Prepared statement result — fast for repeated queries
MySQL.transaction.await(queries)true if every query succeeded
server — safe querieslua
local row = MySQL.single.await('SELECT money FROM characters WHERE id = ?', { charId })

local changed = MySQL.update.await(
    'UPDATE characters SET money = money - ? WHERE id = ? AND money >= ?',
    { price, charId, price }
)
if changed == 0 then return false end  -- not enough money, nothing changed
  • Primary key on every table; index every column you filter or join on.
  • utf8mb4 everywhere so names with emoji do not break inserts.
  • JSON columns for small flexible data; real columns for anything you search or sum.
  • No queries inside per-player or per-frame loops — batch them.

Chapter 10

Frameworks: ESX, QBCore, Qbox and ox_core

Frameworks give you a player object, jobs and money. The ideas are the same everywhere; only the function names differ.

TaskESX LegacyQBCoreQbox
Get playerESX.GetPlayerFromId(src)QBCore.Functions.GetPlayer(src)exports.qbx_core:GetPlayer(src)
Stable IDxPlayer.identifierPlayer.PlayerData.citizenidplayer.PlayerData.citizenid
Add cashxPlayer.addMoney(n)Player.Functions.AddMoney('cash', n)exports.qbx_core:AddMoney(src, 'cash', n)
Remove bankxPlayer.removeAccountMoney('bank', n)Player.Functions.RemoveMoney('bank', n)exports.qbx_core:RemoveMoney(src, 'bank', n)
JobxPlayer.job.name, .gradePlayerData.job.name, .job.grade.levelSame as QBCore
Set jobxPlayer.setJob(job, grade)Player.Functions.SetJob(job, grade)exports.qbx_core:SetJob(src, job, grade)
Loaded event (client)esx:playerLoadedQBCore:Client:OnPlayerLoadedQBCore:Client:OnPlayerLoaded

ox_core is different: characters, groups and bank accounts through Ox.GetPlayer(source) and Ox.GetCharacterAccount(charId), with cash and items as ox_inventory items. To support several frameworks from one script, detect the running one with GetResourceState and route through a small bridge.

server/bridge.lua — one API, three frameworkslua
Bridge = {}

if GetResourceState('qbx_core') == 'started' then
    function Bridge.addCash(src, n) return exports.qbx_core:AddMoney(src, 'cash', n) end
elseif GetResourceState('qb-core') == 'started' then
    local QBCore = exports['qb-core']:GetCoreObject()
    function Bridge.addCash(src, n) return QBCore.Functions.GetPlayer(src).Functions.AddMoney('cash', n) end
elseif GetResourceState('es_extended') == 'started' then
    local ESX = exports.es_extended:getSharedObject()
    function Bridge.addCash(src, n) ESX.GetPlayerFromId(src).addMoney(n) return true end
end

Chapter 11

ox_lib, ox_target and zones

The Overextended libraries remove most boilerplate: callbacks, menus, notifications, progress bars, zones, points, caching and interaction targets.

client — the ox_lib calls you will use mostlua
lib.notify({ title = 'Garage', description = 'Vehicle stored', type = 'success' })

if lib.progressBar({ duration = 3000, label = 'Repairing', canCancel = true }) then
    -- finished
end

local input = lib.inputDialog('Transfer', { { type = 'number', label = 'Amount', min = 1 } })

local zone = lib.zones.box({
    coords = vec3(215.0, -810.0, 30.7), size = vec3(6, 6, 3), rotation = 45,
    onEnter = function() lib.showTextUI('[E] Garage') end,
    onExit = function() lib.hideTextUI() end,
})

local ped = cache.ped  -- cached player ped, updated for you
client — ox_target on a modellua
exports.ox_target:addModel(`prop_atm_01`, {
    {
        name = 'atm:use',
        label = 'Use ATM',
        icon = 'fa-solid fa-credit-card',
        distance = 1.5,
        onSelect = function() TriggerServerEvent('bank:openAtm') end,
    },
})

Zones, targets and text UI run on the client — the server still checks the player is really there before doing anything that matters.

Chapter 12

Items and inventories

Almost every script gives, takes or checks items. A thin inventory layer keeps the rest of your code identical on ox_inventory, qb-inventory and ESX.

Operationox_inventoryqb-inventoryESX default
AddAddItem(src, name, n, meta)AddItem(src, name, n, slot, info, reason)xPlayer.addInventoryItem(name, n)
RemoveRemoveItem(src, name, n)RemoveItem(src, name, n, slot, reason)xPlayer.removeInventoryItem(name, n)
CountGetItemCount(src, name)GetItemCount(src, name)xPlayer.getInventoryItem(name).count
Can carryCanCarryItem(src, name, n)CanAddItem(src, name, n)xPlayer.canCarryItem(name, n)
Server-side; ox_inventory and qb-inventory are called through `exports`.
  • Take the cost first; give the reward only if the removal succeeded.
  • Check carry capacity before adding, or handle the failure.
  • Item names in config — servers rename items.
  • ox_inventory officially supports ox_core, ESX, Qbox and ND_Core; on QBCore, qb-inventory is the default.

Weapons, attachments and ammo live in the inventory too — the FiveM Weapons Handbook covers them in depth.

Chapter 13

NUI: building interfaces

NUI is a Chromium-based browser layer over the game. Any HTML, CSS and JavaScript works — most modern resources use React or another framework built with Vite.

client.lualua
local open = false

local function setOpen(state)
    open = state
    SetNuiFocus(state, state)
    SendNUIMessage({ action = 'visible', data = state })
end

RegisterCommand('bank', function() setOpen(true) end)

RegisterNUICallback('close', function(_, cb)
    setOpen(false)
    cb('ok')
end)

AddEventHandler('onResourceStop', function(res)
    if res == GetCurrentResourceName() and open then SetNuiFocus(false, false) end
end)
web/src/nui.tsts
export async function fetchNui<T>(event: string, data?: unknown): Promise<T> {
  const res = await fetch(`https://${GetParentResourceName()}/${event}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json; charset=UTF-8' },
    body: JSON.stringify(data ?? {}),
  });
  return res.json();
}

window.addEventListener('message', (e) => {
  const { action, data } = e.data;
  if (action === 'visible') setVisible(data);
});
  • Build to static files (web/dist), list them in files and point ui_page at index.html.
  • Use relative asset paths — the page is served from the resource, not a web server root.
  • Avoid backdrop-filter; the in-game browser does not render it reliably. Use solid semi-transparent colours.
  • Every NUI callback always calls cb, or the page’s fetch never resolves.
  • Validate on the server — players can call NUI callbacks with any data from devtools.

More: NUI to Lua patterns, NUI development, and the NUI chapters of the FiveM Server Launch Handbook.

Chapter 14

Writing code that cannot be exploited

Every net event is a public API that anyone connected can call with any arguments. Write each handler as if a cheater will call it — because one will.

server — a handler that survives a cheat menulua
local SHOPS = { legion = vector3(25.7, -1347.3, 29.5) }
local PRICES = { water = 5, bread = 8 }
local last = {}

RegisterNetEvent('shop:buy', function(shopId, item, amount)
    local src = source
    if type(shopId) ~= 'string' or type(item) ~= 'string' or type(amount) ~= 'number' then return end
    amount = math.floor(amount)
    if amount < 1 or amount > 20 then return end

    local shop, price = SHOPS[shopId], PRICES[item]
    if not shop or not price then return end

    if #(GetEntityCoords(GetPlayerPed(src)) - shop) > 5.0 then return end

    local now = GetGameTimer()
    if last[src] and now - last[src] < 1000 then return end
    last[src] = now

    local total = price * amount
    if not Bridge.removeCash(src, total) then return end
    exports.ox_inventory:AddItem(src, item, amount)
end)

AddEventHandler('playerDropped', function() last[source] = nil end)
CheckWhy
Copy source into a local firstIt changes after any Wait or await
Types and rangesCheats send strings, negatives, NaN and huge numbers
Look up prices and rewards on the serverNever trust a price the client sent
Distance, job, item, cooldownThe player must actually be able to do this now
Rate limitStops spam and dupes through repeated calls
Log refusalsRepeated refusals identify cheaters
  • Server-only events: AddEventHandler without RegisterNetEvent.
  • SQL through placeholders only.
  • Secrets in server-only convars (set), never setr/sets or client files.
  • Layers beat a single anticheat: server authority, validation, entity lockdown, game event filtering, identifier + token bans.

Chapter 15

Performance and memory

A good resource idles near 0.00 ms in resmon. Measure first, fix the worst loop, measure again.

resmon CPU (client)Verdict
0.00–0.02 ms idleWhat every idle resource should show
Up to ~0.2 ms while activeFine for UI and interaction
Consistently above ~0.5 msInvestigate — players will notice on busy servers
Open with F8 → `resmon 1`. Server side: `profiler record 500` then `profiler view`.
  • Adaptive Wait — long sleeps when nothing is near.
  • Events, state bags and zones instead of polling.
  • #(a - b) for distances; backtick hashes instead of GetHashKey in loops.
  • Read the ped and coords once per tick (or use ox_lib’s cache).
  • No new tables or strings in per-frame code.
  • Batch database writes; never query per frame.
Finding a leak — memory over time (server)lua
CreateThread(function()
    while true do
        print(('[%s] %.1f KB'):format(GetCurrentResourceName(), collectgarbage('count')))
        Wait(60000)
    end
end)

Growing memory almost always means per-player tables not cleared on playerDropped, handlers registered inside other handlers, unbounded caches, or entities and blips never deleted.

Chapter 16

Debugging and common errors

Client errors show in F8, server errors in the server console or txAdmin’s live console. The message tells you the file and line — read it before changing code.

ErrorMeaningUsual fix
attempt to index a nil valueUsed . or [] on nilThe object was never set — check the lookup before it
attempt to call a nil valueThe function does not existTypo, wrong side (client/server), or missing dependency
attempt to perform arithmetic on a nil valueA number you used is nilValidate inputs; give defaults
attempt to concatenate a nil value.. with nilUse tostring() or check first
No such export X in resource YExport missing or resource not startedCheck the name and start order
Couldn’t find resourceWrong name in ensureFolder name, not the manifest name
Syntax error near endUnbalanced if/function/endLet LuaLS highlight it
  • Print with context: print(('[garage] store %s by %d'):format(plate, src)).
  • lib.print.debug with a debug convar keeps logs out of production.
  • Reproduce on the local server before touching the live one.

Chapter 17

HTTP APIs, Discord webhooks and scheduled jobs

Servers talk to the outside world: Discord logs, stores, whitelists, stats. Do it from the server, keep keys private and never block the server thread.

server — a Discord loglua
local HOOK = GetConvar('logs_webhook', '')

local function log(title, text, color)
    if HOOK == '' then return end
    PerformHttpRequest(HOOK, function(status)
        if status == 429 then print('[logs] rate limited') end
    end, 'POST', json.encode({
        embeds = { {
            title = title,
            description = text,
            color = color or 8150271,
            timestamp = os.date('!%Y-%m-%dT%H:%M:%SZ'),
        } },
    }), { ['Content-Type'] = 'application/json' })
end
server — clock-based jobs with ox_liblua
lib.cron.new('0 6 * * *', function()
    MySQL.update.await('UPDATE characters SET daily_claimed = 0')
end)
  • PerformHttpRequest(url, cb(status, body, headers, err), method, data, headers) — wrap it in a promise + Citizen.Await for a synchronous style.
  • Webhook URLs and API keys in set convars only.
  • Respect Discord limits (for example 4096 characters per description, 10 embeds per message) and back off on 429.
  • SetTimeout for one-off delays, loops with Wait for intervals, lib.cron for clock times.

Chapter 18

Config, locales, documentation and review

What separates a script people recommend from one they refund: a config owners understand, their language, docs that answer questions before they are asked, and code someone else checked.

config.lua — values, not logiclua
Config = {}

Config.Locale = 'en'
Config.Framework = 'auto'   -- auto | qbx | qb | esx
Config.Inventory = 'auto'   -- auto | ox | qb | esx

Config.Garages = {
    legion = { label = 'Legion Square', coords = vec4(215.8, -810.1, 30.7, 157.0), jobs = false },
    mrpd   = { label = 'Mission Row PD', coords = vec4(452.3, -996.1, 25.8, 90.0), jobs = { police = 0 } },
}
  • Secrets and reward amounts in a server-only config.
  • Validate the config on start and print readable errors.
  • ox_lib locales: ox_lib 'locale' in the manifest, locales/en.json, locale('key'); owners choose with setr ox:locale de.
  • Docs: requirements, install steps with ensure order, every config option, commands, exports and events, troubleshooting, changelog.
  • LuaLS annotations (---@param, ---@class) catch bugs before players do.

Before every release, run the code review checklist: security, performance, cleanup, compatibility, usability.

Chapter 19

Shipping and selling: escrow, Tebex, pricing, clients

Selling FiveM scripts runs through Tebex and the Cfx.re Portal’s asset escrow. Know the rules before you build a business on them.

FactDetail
What escrow encryptsLua, YFT, YDD and YDR files
What stays readableFiles listed in escrow_ignore, plus NUI files
DeliveryThrough Tebex; buyers receive the asset in their Cfx.re Portal account
Without the entitlementThe server refuses to start it: “You lack the required entitlement”
Upload size limit1 GB per asset
MonetizationPaid FiveM content is sold through Tebex under the platform’s licence terms
  1. Create a Tebex account, verify it, and link your Cfx.re account.
  2. Upload the zipped resource to the Portal to escrow it.
  3. Create a Tebex package that delivers the escrowed asset.
  4. Launch with a video, documentation and a support channel.

Pricing, contracts and portfolios: script pricing, freelance contracts, developer portfolio.

Chapter 20

Your learning path

Learn in this order and each step builds on the last. Skipping ahead is why so many scripts work on a test server and break on a live one.

StageLearnBuild
1Lua, resources, your first scriptA command that spawns a car and prints its plate
2Client vs server, events, natives, threadsA delivery job paid by the server
3A framework, ox_lib, oxmysqlA garage that stores vehicles in the database
4NUIA React menu for the garage
5Security and performanceHarden it; get it to 0.00 ms idle
6Git, local server, LuaLSVersion it; test like a professional
7Docs, escrow, Tebex, portfolioRelease it — free first, paid later

Where to learn more: the official FiveM docs, the natives reference, the Lua 5.4 manual, framework docs and the Cfx.re forum.

Checklist

  • I declare every variable local
  • My client never decides money, items or permissions
  • Every net event validates source, types, ranges, distance and cooldown
  • All SQL uses placeholders
  • My idle resources show 0.00–0.02 ms in resmon
  • I clean up on playerDropped and onResourceStop
  • My code is in Git and my secrets are not

Reference

Cheat sheets

The functions you will look up again and again.

Events and communication

Which function to call, from which side.

FunctionSideWhat it does
RegisterNetEvent(name, fn)BothHandle an event that may arrive over the network
AddEventHandler(name, fn)BothHandle a local event
TriggerEvent(name, ...)BothFire on the same side
TriggerServerEvent(name, ...)ClientSend to the server
TriggerClientEvent(name, id, ...)ServerSend to one player, or -1 for all
TriggerLatentClientEvent(name, id, bps, ...)ServerSend large data without flooding the connection
lib.callback.register / lib.callback.awaitBothRequest and answer (ox_lib)
exports('Name', fn) / exports.res:Name()Same sideCall functions across resources
SendNUIMessage(table) / RegisterNUICallbackClientTalk to and from the NUI page

Lifecycle events

Hooks for setup and cleanup.

EventSideFires when
onResourceStartBothA resource starts (check the name argument)
onResourceStopBothA resource stops — clean up here
playerConnectingServerA player is joining — deferrals, whitelists, bans
playerJoiningServerThe player has a server ID and is loading in
playerDroppedServerA player left — clear their data
txAdmin:events:scheduledRestartServerBefore a scheduled restart
entityCreatingServerAn entity is about to be created — cancel to block

oxmysql at a glance

All awaitable on the server; placeholders always.

CallReturns
MySQL.query.await(sql, p)Rows
MySQL.single.await(sql, p)One row or nil
MySQL.scalar.await(sql, p)One value
MySQL.insert.await(sql, p)Insert ID
MySQL.update.await(sql, p)Affected rows
MySQL.prepare.await(sql, p)Prepared result
MySQL.transaction.await(list)true / false

Natives you will use constantly

Client unless marked server.

NativeUse
PlayerPedId() / GetPlayerPed(src) (server)The player’s ped
GetEntityCoords(ent) / GetEntityHeading(ent)Position and facing
GetPlayerIdentifierByType(src, 'license') (server)A stable player ID
IsPlayerAceAllowed(src, 'object') (server)Permission check
RequestModel(hash) / HasModelLoaded(hash)Load a model before spawning
NetworkGetNetworkIdFromEntity / NetworkGetEntityFromNetworkIdConvert handles and network IDs
SetNuiFocus(hasFocus, hasCursor)Give NUI keyboard and mouse
RegisterKeyMapping(cmd, desc, 'keyboard', 'E')Rebindable key for a command
GetConvar(name, default)Read a convar
GetGameTimer()Milliseconds — for cooldowns

Reference

Glossary

CfxLua
FiveM’s Lua 5.4 fork with vectors, quaternions and compile-time backtick hashes.
Callback
A request that gets an answer from the other side — lib.callback, ESX and QBCore callbacks.
Client script
Code that runs on each player’s PC; never trusted.
Convar
A configuration variable set in server.cfg and read with GetConvar.
Deferrals
The playerConnecting API for holding a joining player while you run checks.
Entity
A ped, vehicle or object in the game world.
Entity lockdown
sv_entityLockdown — stops clients creating entities.
Escrow
Cfx.re’s encryption and entitlement system for paid resources.
Event
A named message between scripts or between client and server.
Export
A function one resource makes callable by others on the same side.
fxmanifest.lua
The file that declares a resource’s scripts, files and dependencies.
GlobalState
The server-wide state bag.
Hash
The number the game uses for a model or name; `adder` in CfxLua.
KVP
Key-value storage per resource via SetResourceKvp.
LuaLS
The Lua Language Server that powers VS Code completion and diagnostics.
Native
A built-in game or Cfx.re function such as GetEntityCoords.
Network ID
The ID of a networked entity that is the same on every machine.
NUI
The in-game browser layer for interfaces.
NUI callback
A Lua handler the page calls with fetch('https://resource/name').
OneSync
Server-side state awareness; enables server entities and state bags.
Owner
The client that simulates a networked entity.
ox_lib
Overextended’s library of UI, callbacks, zones, cron and helpers.
ox_target
Overextended’s interaction targeting system.
oxmysql
The database connector with MySQL.*.await functions.
Placeholder
A ? or @name in SQL that keeps values as data.
Resource
A folder with an fxmanifest.lua — the unit FiveM starts and stops.
resmon
The in-game resource monitor (resmon 1 in F8).
Routing bucket
An instance of the world; players and entities in different buckets do not see each other.
Server script
Code that runs only on the server; the authority.
source
The ID of the player that sent a net event, inside its handler.
State bag
Synced key-value data on the server, a player or an entity.
Tebex
The store platform FiveM content is sold through.
Thread
A coroutine started with CreateThread, paused with Wait.
Zone
An area (box, sphere, poly) that triggers enter/exit/inside code.

Reference

Questions developers ask

What language are FiveM scripts written in?

Mostly Lua (CfxLua, based on Lua 5.4). JavaScript/TypeScript and C# are also supported.

How do I start FiveM scripting?

Install VS Code with the Lua Language Server, run a local FXServer, create a resource with an fxmanifest.lua and a client and server script, and ensure it.

What is the difference between client and server scripts?

Client scripts run on each player’s PC and handle what they see and do; server scripts run on the server and own money, items, the database and permissions.

How do I send data from client to server?

TriggerServerEvent('name', ...) on the client and RegisterNetEvent('name', function(...) end) on the server, reading source for the sender.

How do I get a value back from the server?

Use a callback: lib.callback.register on the server and lib.callback.await on the client (or your framework’s callbacks).

What are natives?

The game’s built-in functions, listed at docs.fivem.net/natives and called in PascalCase, like GetEntityCoords.

Why does my script use so much resmon?

Usually a Wait(0) loop running when it does not need to. Sleep longer when nothing is near, or replace the loop with events or zones.

How do I use the database in FiveM?

With oxmysql: MySQL.query.await('SELECT ... WHERE id = ?', { id }) on the server, always with placeholders.

How do I make my script work on ESX and QBCore?

Detect the framework with GetResourceState and route money, job and item calls through a small bridge.

How do I make a NUI in FiveM?

Build an HTML/React page, list it with ui_page and files, send data with SendNUIMessage, receive calls with RegisterNUICallback, and manage focus with SetNuiFocus.

How do I stop cheaters triggering my events?

Validate everything in the server handler: types, ranges, server-side prices, distance, permissions and cooldowns — and never trust client values.

What are state bags?

Synced key-value data on the server, players or entities, set on the server and read or watched on clients.

How do I sell FiveM scripts?

Escrow the resource through the Cfx.re Portal and sell it on Tebex with documentation and support.

What does escrow encrypt?

Lua, YFT, YDD and YDR files; config files you list in escrow_ignore and NUI files stay readable.

Should I learn TypeScript for FiveM?

Learn Lua first; add TypeScript for NUI and for server code that benefits from npm packages.

How do I debug a FiveM script?

Read the error in F8 (client) or the server console, check the file and line, add printed context, and reproduce on a local server.

Where can I learn FiveM development?

docs.fivem.net, the natives reference, the Lua manual, framework docs, the Cfx.re forum — and the detailed guides linked in each chapter.

How long does it take to become a FiveM developer?

Simple scripts take weeks; secure, fast, sellable resources usually take months of steady practice.

Reference

Official references

Primary sources this handbook is checked against.

Official Cfx.re documentation

Libraries and frameworks

Language

Community

The FiveM Handbook series

Four free handbooks, one per part of the job.

Need this built, not just explained?

Custom FiveM resources, NUI and security reviews

Lua and React NUI for ESX, QBCore and Qbox, OneSync performance audits, and the websites and SEO around your server brand.