This guide walks you through creating a server-authoritative QBCore mechanic job resource: repair, tow, and impound actions, qb-target interactions, and safe server-side validation. You'll get a short plan, assumptions, complete files to drop into a resource, installation steps, testing checklist, and security considerations.
Implementation plan
1) Define config and permissions. 2) Register qb-target interactions (repair/tow/impound). 3) Use server callbacks to check job, charge money, and record impounds. 4) Client performs the local vehicle actions only after server approval. 5) Provide a simple persistent impound hook placeholder (oxmysql or your vehicle ownership system).
Assumptions and dependencies
- Server uses QBCore (v1/v2 style exports). Adjust GetCoreObject line to match your core.
- qb-target or ox_target installed for targeting interactions.
- Optional: oxmysql or your own persistence if you want permanent impound storage — otherwise impounds will be ephemeral.
- The mechanic is a job named "mechanic" in your jobs config. Change job name in
config.luaif different.
Destination-labelled files (complete)
Create a resource folder called qb-mechanic. Place these files exactly as named.
fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
author 'YourName'
description 'QBCore mechanic job (repair, tow, impound)'
version '1.0.0'
shared_script 'config.lua'
client_script 'client/main.lua'
server_script 'server/main.lua'
dependencies {
'qb-core',
'qb-target'
}
config.lua
Config = {}
Config.JobName = 'mechanic'
Config.RepairCost = 150 -- cash amount
Config.TowCost = 250
Config.ImpoundCost = 500
Config.ImpoundLocation = vector3(412.0, -1634.0, 29.3) -- example
Config.TargetDistance = 3.0
client/main.lua
local QBCore = exports['qb-core']:GetCoreObject()
-- qb-target registration for nearby vehicles (run on client start)
CreateThread(function()
while not NetworkIsSessionStarted() do
Wait(100)
end
-- register an entity target for vehicles the player is near
exports['qb-target']:AddTargetModel(GetHashKey('a_m_m_business_01'), {
options = {
{
type = 'client',
event = 'qb-mechanic:client:OpenMechanicMenu',
icon = 'fas fa-wrench',
label = 'Mechanic Actions',
}
},
distance = 3.0
})
end)
-- Client event opens actions and coordinates server validation
RegisterNetEvent('qb-mechanic:client:OpenMechanicMenu', function()
local playerPed = PlayerPedId()
local veh = GetVehiclePedIsIn(playerPed, true)
local plate = veh ~= 0 and QBCore.Functions.GetPlate(veh) or nil
local elements = {}
table.insert(elements, {label = 'Repair Vehicle ($' .. Config.RepairCost .. ')', value = 'repair'})
table.insert(elements, {label = 'Tow Vehicle ($' .. Config.TowCost .. ')', value = 'tow'})
table.insert(elements, {label = 'Impound Vehicle ($' .. Config.ImpoundCost .. ')', value = 'impound'})
-- simple menu using ox_lib or your UI. Here we call server directly per choice.
-- Replace this menu with your preferred UI. For example purposes we'll use a native input.
local choice = nil
-- (Replace the following placeholder with your UI selection implementation.)
-- For demo, we will always pick repair if in a vehicle, otherwise impound is skipped.
if veh ~= 0 then
choice = 'repair'
else
QBCore.Functions.Notify('Stand near a vehicle to use mechanic actions', 'error')
return
end
if choice == 'repair' then
-- ask server to charge and approve
QBCore.Functions.TriggerCallback('qb-mechanic:server:ChargeForService', function(success)
if success then
-- local repair action: server authorised
SetVehicleFixed(veh)
SetVehicleDirtLevel(veh, 0.0)
QBCore.Functions.Notify('Vehicle repaired', 'success')
else
QBCore.Functions.Notify('You cannot perform this action', 'error')
end
end, 'repair')
end
end)
server/main.lua
local QBCore = exports['qb-core']:GetCoreObject()
local Impounded = {} -- in-memory store; replace with DB for persistence
QBCore.Functions.CreateCallback('qb-mechanic:server:ChargeForService', function(source, cb, serviceType)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then cb(false); return end
-- job validation
if Player.PlayerData.job.name ~= Config.JobName then
cb(false)
return
end
local cost = 0
if serviceType == 'repair' then cost = Config.RepairCost
elseif serviceType == 'tow' then cost = Config.TowCost
elseif serviceType == 'impound' then cost = Config.ImpoundCost
else cb(false); return end
-- check money and remove
if Player.Functions.RemoveMoney('cash', cost) then
cb(true)
else
cb(false)
end
end)
-- Impound example: record an impound and notify
RegisterNetEvent('qb-mechanic:server:ImpoundVehicle', function(plate, model, owner)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
if Player.PlayerData.job.name ~= Config.JobName then return end
-- place into in-memory table; replace with oxmysql insert for persistence
table.insert(Impounded, {plate = plate, model = model, impoundedBy = Player.PlayerData.citizenid, time = os.time()})
TriggerClientEvent('QBCore:Notify', src, 'Vehicle impounded: ' .. plate, 'success')
-- TODO: save to DB via exports.oxmysql:insert(...) if you want persistence across restarts
end)
Where files belong and install
- Create a folder resources/[your-folder]/qb-mechanic and drop fxmanifest.lua, config.lua, client/main.lua, server/main.lua inside.
- Add start qb-mechanic to your server.cfg (ensure qb-core and qb-target are started first).
- Restart server or resource and test.
Testing & validation checklist
| Step | What to expect |
|---|---|
| Start resource | No startup errors in server console |
| Open mechanic menu near a vehicle | Menu opens and repair option available |
| Repair action | Money removed server-side and vehicle fixed client-side |
| Impound action | Entry added to server Impounded table (or DB if integrated) |
| Unauthorized job try | Action rejected and no money removed |
Security considerations
- Never let the client remove or add money. Server callbacks must do all monetary operations (as shown).
- Validate job and permissions on the server for every paid action.
- If you need to validate vehicle ownership before impound, query your ownership DB server-side rather than trusting client-sent plate strings.
- For persistence use oxmysql or your preferred DB with proper prepared statements to avoid injection.
- Remember that the client still performs local vehicle fixes. For strict enforcement, set a server-side state that flags repaired vehicles and reject inconsistent updates from anti-cheat logic.
Next steps and useful integrations
- Integrate a full UI (ox_lib or your custom NUI) for selecting actions and showing receipts.
- Hook impound storage into your owned_vehicles or a dedicated impound table in the DB for persistent retrieval.
- Add job blips, ranks, and a garage for professional tools and upgradeable equipment.
If you'd like help designing the full feature set or producing destination-labelled files for an extended mechanic resource (NUI, persistent DB, tow-trailer handling), try building the plan in the Stellar AI app and iterate quickly. The app helps translate plain-English requirements into complete resource files you can drop into your server. After that, check our walkthroughs on the blog for integration patterns: Stellar AI blog.
When you're ready to prototype multi-role flows or export a staged resource, open the generator in the Stellar AI app to prototype file sets and installation instructions you can copy directly into your FiveM server.