A practical guide to how the qbcore job system works — complete guide, with implementation decisions, validation steps, and security considerations for a production-minded project.
Overview
This guide explains how a QBCore job system works in a FiveM server, covering architecture, implementation choices, security and server validation, testing, and maintenance. It also highlights differences and important considerations for Roblox implementations (Luau) where relevant. The goal is practical: show patterns you can copy, validate, and maintain safely.
Core architecture of a QBCore job system
A robust job system separates responsibilities across layers. Typical components include:
- Job definitions (shared data): names, grades, permissions, pay scales, and available tasks.
- Server-side authority: job assignment, payout calculation, inventory changes, and database persistence.
- Client-side UI: markers, menus, task progression visuals — strictly presentation only.
- Persistence layer: MySQL or an ORM, with transactions for critical changes and backups for job-related payouts.
- Integration layer: optional exports to ox_lib, ESX compatibility shims, and admin tools.
Design principle: the server is authoritative. Never assume a client honestly reports job progress, money, or inventory changes.
QBCore components and job definitions
In QBCore, define jobs in a shared config file so both client and server can reference the same structure. Keep sensitive values (pay formula, limits) on the server only. Example definition elements:
- job name (string)
- grades (array: permissions, salary multiplier)
- actions (table: task identifiers mapped to server-side handlers)
- locations (coords used by clients for UI only)
-- shared/jobs.lua (example)
Jobs = {
garbage = {
label = "Garbage Collector",
grades = { ['0'] = {pay = 50}, ['1'] = {pay = 75} },
tasks = { "collect_bin", "deliver_trash" },
locations = { start = vector3(10.0, 6500.0, 31.0) }
},
-- other jobs
}
Server-side validation & security
Security is the most important part of a job system. Treat any client-sent event as untrusted input. The server must validate:
- Player identity and job name using QBCore.Functions.GetPlayer(source)
- That the player's job grade permits the requested action
- No modification of payout values from the client — compute payouts server-side
- Rate-limit high-value events and use anti-spam logic
- Use prepared statements / parameterized queries for DB writes
Example server pattern (QBCore):
-- server/main.lua
local QBCore = exports['qb-core']:GetCoreObject()
RegisterNetEvent('qb-jobs:server:completeTask', function(taskId)
local src = source
if not src then return end
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local jobName = Player.PlayerData.job.name
-- validate job and task ownership on server
if jobName ~= 'garbage' then return end
-- map taskId to server-side reward & checks
local task = Jobs.garbage.tasks[taskId]
if not task then return end
local reward = CalculateServerReward(jobName, Player.PlayerData.job.grade.level, taskId)
if reward <= 0 then return end
-- apply money server-side and persist transaction
Player.Functions.AddMoney('cash', reward, "job-complete-" .. taskId)
end)
Key points: perform all payout calculation and permission checks on the server. Log suspicious behavior and disconnect or flag for review instead of rewarding unexpected values.
Client-side behavior and UI
Client scripts should be thin: spawn markers, play animations, and send minimal events to the server indicating attempts or confirmations. Never send reward values, and avoid sending unobfuscated task progress that could be modified. When a client submits "task complete", the server should verify all aspects before any state change.
Use client-side movement checks and simple visual validation for UX, but do not treat these as authoritative.
Integration choices: ox_lib, ESX, and interoperability
When integrating with ox_lib or providing an ESX compatibility layer, keep the QBCore server logic central and expose safe exports only. For example:
- Provide an export for reading job definitions (read-only)
- Expose server callbacks for admin tools that require permission checks
- Avoid exposing any function that directly mints currency on the client request — require server callbacks that perform permission checks
If you must support ESX players, translate data at the server boundary and maintain one canonical job state; do not duplicate logic across frameworks.
Implementation choices: database, permissions, grades
Decisions you will need to make:
- DB schema: store PlayerData.job.name and grade, job logs, and payout history. Use transactions when modifying both balance and job progress.
- Grades & permissions: implement a simple RBAC model — grades map to permission flags. Do not assume numeric grade implies permissions; check flags by name.
- Task definitions: identify tasks by server-side IDs rather than client names; allow server to change task parameters without client updates.
Maintain migrations for job schema changes to ensure upgrades don't break running servers.
Testing, debugging, and anti-cheat testing
Testing should include automated unit tests for server-side logic and manual scenario tests for client/server interactions.
- Simulate malicious clients by sending invalid events and verify the server rejects them.
- Unit test payout calculations and DB transactions.
- Use sandbox environments to test migrations and new job definitions before deploying to production.
Log minimal, structured information on failures (error codes, player id, event name) and aggregate logs for pattern detection. If you use an external monitoring dashboard consider low-latency hooks; for example, a development tool or a hosted app can help visualize job activity — try the app here: Stellar AI App.
Roblox considerations: RemoteEvents and DataStore robustness
When porting job concepts to Roblox:
- RemoteEvents are the only acceptable client→server path. The first parameter for OnServerEvent is always the player — use it to validate identity.
- Never accept client-calculated rewards. Calculate on the server and then use RemoteEvent:FireClient to confirm.
- DataStore failures are common; wrap writes in pcall, implement exponential backoff, batched writes, and queuing for critical transactions. Avoid losing money changes on failure by keeping an in-memory queue with write retries.
- Rate-limit RemoteEvents per player and monitor for abnormal frequency.
-- Roblox server-side pattern (Luau)
local Remote = game.ReplicatedStorage:WaitForChild("CompleteTask")
Remote.OnServerEvent:Connect(function(player, taskId)
local playerJob = GetPlayerJob(player) -- server-side
if playerJob ~= "Delivery" then return end
local reward = CalculateRewardForTask(taskId, playerJob)
-- persist with DataStore, wrapped in pcall
local success, err = pcall(function()
-- DataStoreSet implementation
end)
if not success then
-- queue retry or inform player via RemoteEvent
else
GivePlayerMoney(player, reward)
end
end)
Deployment and maintenance
Deployment checklist:
- Use a CI pipeline to deploy server scripts and DB migrations.
- Keep configuration values for pay scales and limits in a versioned config file.
- Have a rollback plan: a migration that reverts job schema changes quickly.
- Monitor error logs, suspicious behavior alerts, and latency for DB writes.
For centralized dashboards and operational workflows, you can integrate server logs or metrics into an external service. If you need a hosted monitoring and deployment assistant, the following app can be useful: Stellar AI App. For editorial updates and deeper articles on server architecture, see: Stellar AI Blog.
Practical checklist
| Item | Why it matters | Action |
|---|---|---|
| Server-side job validation | Prevents unauthorized rewards | Verify job name and grade with QBCore.Functions.GetPlayer before actions |
| Server-calculated payouts | Clients can be manipulated | Map taskId → server payout; ignore client-sent amounts |
| DB transactions | Atomicity for money & progress changes | Use transactional writes or rollback on failure |
| Rate limiting | Prevents spam and automated abuse | Track last event timestamps per player and block rapid repeats |
| Logging & monitoring | Detect exploits | Log structured events; aggregate with dashboards |
| Roblox DataStore retries | Network errors are common | Use pcall, retry with backoff, and queue unsaved changes |
Common pitfalls and how to avoid them
- Trusting client payloads: Never perform financial operations based on client values. The server should compute and execute.
- Lack of auditing: Not logging job payouts or suspicious events makes it impossible to post-mortem exploits. Keep concise logs for important transactions.
- Mixing frameworks without boundary: If you support both QBCore and ESX, keep a single authoritative module for job logic and only adapt interfaces per framework.
- Ignoring DataStore retries (Roblox): Always handle failure paths; do not assume a write always succeeds.