A practical guide to how to add an ambulance job to your qbcore fivem server, with implementation decisions, validation steps, and security considerations for a production-minded project.
Introduction
This guide walks you through adding a robust, secure ambulance job to a QBCore FiveM server. It covers architecture choices, server-authoritative patterns, integration points (vehicles, garage, revives, billing), testing, and deployment considerations. Examples use modern QBCore patterns and emphasize never trusting client input — the server must validate all actions. If you want a GUI or management tooling during development, try the app at https://trystellarai.com/app for workflow help and later check the blog for best practices at https://trystellarai.com/blog.
High-level architecture
Design the ambulance feature as a cohesive resource (for example qb-ambulance) with a clear separation of responsibilities:
- Server authority: job registration, permissions, billing, inventory changes, database writes.
- Client responsibility: animations, menus, display-only UI, local effects (not authoritative actions).
- Shared definitions: job names, labels, configuration constants, and locales.
Keep the server side minimal and authoritative: clients send intents (e.g., "I want to revive player X") and the server confirms legitimacy, performs checks, and executes the action.
QBCore job registration and data model
QBCore stores jobs in shared configuration. You can register the ambulance role in your project's shared jobs file or as part of your resource. Typical steps:
- Add an entry to shared/jobs.lua (or your resource's config) with sensible permissions and labels.
- Create role-specific configuration: equipment, vehicles, revival cost, allowed items.
- Ensure database consistency: existing player job entries must match the new job name. If migrating, update your DB rows to the new job name prior to deploying.
Example snippet (conceptual):
-- shared/jobs.lua (illustrative)
jobs['ambulance'] = {
label = "EMS",
defaultDuty = false,
grades = {
['0'] = { name = "Probationary", salary = 50 },
['1'] = { name = "Medic", salary = 100 },
['2'] = { name = "Doctor", salary = 150 }
}
}
Server-side validation and security
Never trust client input. All client-initiated actions must be verified server-side before applying effects. Common attack vectors include falsified heal events, forged bills, and unauthorized vehicle spawns. Mitigations:
- Validate the caller's job and grade on every server event.
- Check proximity: confirm the target player is within an expected range on the server (send both player coords from server and compare).
- Rate-limit critical actions (e.g., revives) and require cooldowns or resource consumption.
- Log unusual activity server-side for audit and possible automated bans/warnings.
Example server event that validates authority and range before reviving:
local QBCore = exports['qb-core']:GetCoreObject()
RegisterNetEvent('qb-ambulance:server:requestRevive', function(targetId)
local src = source
local player = QBCore.Functions.GetPlayer(src)
if not player or player.PlayerData.job.name ~= 'ambulance' then
return -- unauthorized
end
local target = QBCore.Functions.GetPlayer(tonumber(targetId))
if not target then return end
-- Server-side proximity check: request coords from both players or use server tracking
-- Here we ask the client for their coords, but verify with additional server-side logic or distance data
TriggerClientEvent('qb-ambulance:client:confirmRevive', src, target.PlayerData.source)
end)
Implementation choices: vehicles, spawners, and ox_lib
Decide how ambulances are provided. Common options:
- Static parking spots with a server-side spawner that checks job and duty status.
- Use a garage resource to manage keys and storage. Ensure keys are server-tracked.
- Optionally integrate ox_lib for menus and progress bars; keep its use client-side for UX only.
Vehicle spawn should be a server-triggered action after validation. Example pattern:
-- Server: spawn vehicle if job and spot available
RegisterNetEvent('qb-ambulance:server:spawnAmbulance', function(model)
local src = source
local xPlayer = QBCore.Functions.GetPlayer(src)
if not xPlayer or xPlayer.PlayerData.job.name ~= 'ambulance' then return end
-- server can check a "spawn queue" to avoid abuse and ensure unique instances
TriggerClientEvent('qb-ambulance:client:spawnVehicle', src, model)
end)
Healing, revives, and billing patterns
Typical ambulance responsibilities: heal, revive, transport, and billing. For each action, follow this pattern:
- Client sends intent to server (e.g., "I wish to bill player X for Y").
- Server verifies job, proximity, target status, and amount (ensure non-negative, within configured caps).
- Server performs account transactions, database writes, and triggers client effects.
Example server-side billing validation:
RegisterNetEvent('qb-ambulance:server:billPlayer', function(targetId, amount)
local src = source
local biller = QBCore.Functions.GetPlayer(src)
amount = tonumber(amount)
if not biller or biller.PlayerData.job.name ~= 'ambulance' then return end
if not amount or amount <= 0 or amount > 5000 then return end -- cap
local target = QBCore.Functions.GetPlayer(tonumber(targetId))
if not target then return end
-- perform the transaction server-side
target.Functions.RemoveMoney('bank', amount, "ambulance-bill")
biller.Functions.AddMoney('bank', amount, "ambulance-income")
-- record billing in DB or invoice system
end)
File structure and required entries
Keep a predictable layout in your ambulance resource. Example minimal structure:
| File / Resource | Purpose |
|---|---|
| fxmanifest.lua | Define resource, dependencies (qb-core, ox_lib optional), and server/client scripts |
| server/main.lua | All authoritative logic: job checks, billing, revives, DB writes |
| client/main.lua | Animations, progress bars, UI, vehicle spawning request (non-authoritative) |
| config.lua | Job name, revive costs, vehicle models, spawn points |
| locales/*.lua | Localization for messages and notifications |
Ensure fxmanifest enumerates dependencies to prevent startup order issues. Example dependency line: dependency 'qb-core'.
Testing and debugging
Follow a staged testing workflow:
- Start with server-only unit checks: validate config values and DB migrations.
- Use a local client to exercise each server event and inspect server logs (server console & qb-core debug). Add verbose logging temporarily around validation paths.
- Test edge cases: unauthorized client attempts, missing target, invalid amounts, distance manipulation.
- Simulate network issues: what happens if a client disconnects mid-revive? Ensure server rolls back partial transactions.
Use these debug tips:
- Print contextual info server-side (source id, job, target id) for event handling.
- Use breakpoints in your editor with a Lua debugger if available.
- Review server event flood patterns to detect abuse.
Deployment and maintenance
Before deploying to production:
- Back up your database (players and job fields).
- Deploy to a staging server and allow staff to validate job behavior.
- Plan for migrations: document any DB updates required for job names or permissions.
Ongoing maintenance items:
- Monitor server logs for unauthorized attempts or errors.
- Keep the resource compatible with qb-core updates; test after core updates.
- Maintain security: patch any server-side logic that makes assumptions about client state.
For management and workflow tooling during your build/deploy cycle, you can pair local dev tools with the developer dashboard at https://trystellarai.com/app.
Roblox (Luau) considerations — brief
If you also maintain a Roblox server or adapt patterns from this guide to Roblox, follow these authoritative server principles:
- Use RemoteEvents/RemoteFunctions only for intent; perform all state changes (health, currency) on the server.
- Treat client messages as untrusted and perform proximity/permission checks server-side.
- Handle DataStore failures using pcall and exponential backoff, and plan for writes to be retried or queued on server restart.
- Never assume a successful DataStore write — record local transient state and ensure reconciliation on player reconnect.
Practical checklist before going live
| Task | Done | Notes |
|---|---|---|
| Register ambulance job in shared jobs list | Ensure name matches DB entries or migrate DB | |
| Implement server-side revive/billing handlers | Include job & proximity validation | |
| Add vehicle spawn server-side checks | Limit spawn rate and ensure unique instances | |
| Test unauthorized client attempts | Log and hard-fail on suspicious patterns | |
| Deploy on staging and run full playtest | Collect staff feedback and adjust |
Common pitfalls and how to avoid them
Watch out for:
- Client-only checks for billing or revives — always duplicate checks server-side.
- Hardcoding job names in multiple places — centralize in config/shared files.
- Not handling disconnects during critical flows — ensure atomic server operations.
- Mismatched dependencies or missing fxmanifest entries causing startup failures.
Example server-to-client safe flow
Flow pattern summary:
- Client requests action → Server validates job, proximity, caps → Server performs DB changes and triggers client effects → Server logs event.
- Client only displays UI and animations after confirmation from the server.