Designing the Mechanic Job Architecture
A mechanic job system is one of the most popular and rewarding features to develop for a FiveM roleplay server. Unlike simple repair scripts that just call SetVehicleFixed(), a proper mechanic system introduces an entire job economy with specialized roles, part inventories, and player-to-player interactions that drive roleplay. The architecture needs to account for multiple mechanic shops across the map, each potentially owned by different player groups, with a job grade hierarchy that determines who can perform which repairs. At the foundation, you need a shared configuration that defines shop locations, available services at each tier, parts pricing, and the job grades required to unlock advanced repairs like engine swaps or turbo installations. The system should integrate with your server's existing job framework, whether that is QBCore, ESX, or a custom solution, so that players clock in and out of the mechanic role with proper paycheck handling and duty status tracking.
Job Grades and Permission System
A tiered job grade system gives your mechanic job depth and encourages players to work their way up through the ranks. Start by defining clear grades with specific permissions: a trainee might only be able to wash vehicles and repair minor body damage, a standard mechanic can handle engine repairs and tire replacements, a senior mechanic unlocks performance tuning and spray painting, and the shop owner can manage employees and set service pricing. Store these grade definitions in a shared config so both client and server can reference them consistently. Here is an example configuration that maps grades to their allowed services:
Config.MechanicGrades = {
[0] = {
label = 'Trainee',
services = {'wash', 'body_minor'},
payMultiplier = 0.7
},
[1] = {
label = 'Mechanic',
services = {'wash', 'body_minor', 'body_major', 'engine_repair', 'tire_replace', 'brake_repair'},
payMultiplier = 1.0
},
[2] = {
label = 'Senior Mechanic',
services = {'wash', 'body_minor', 'body_major', 'engine_repair', 'tire_replace',
'brake_repair', 'spray_paint', 'performance_tune', 'turbo_install'},
payMultiplier = 1.3
},
[3] = {
label = 'Shop Owner',
services = 'all',
payMultiplier = 1.5,
canManage = true
}
}
Config.ServicePrices = {
wash = 200,
body_minor = 800,
body_major = 2500,
engine_repair = 3500,
tire_replace = 1200,
brake_repair = 1500,
spray_paint = 5000,
performance_tune = 15000,
turbo_install = 25000
}
On the server side, always validate the player's grade before allowing any service to execute. Never trust the client to report which services are available because a cheater could modify the NUI to send any service request. The server callback should check the player's current job, their grade level, and whether the requested service exists in their grade's service list before proceeding with the repair.
Vehicle Repair Mechanics and Parts System
Realistic repair mechanics go beyond a single native call. Instead of instantly fixing a vehicle, break the repair process into steps that require time and consume parts from the mechanic's inventory. When a player brings their car in for an engine repair, the mechanic should need to have the correct parts in their personal inventory or in the shop's shared stash. Use a progress bar system to simulate the repair taking time, which creates natural roleplay moments where the customer waits and interacts with the mechanic. The repair logic should target specific vehicle components using native functions so that you can repair the engine without fixing body damage, or replace tires without affecting the engine health:
local RepairFunctions = {
engine_repair = function(vehicle)
local parts = {'engine_oil', 'spark_plugs', 'coolant'}
if not HasRequiredParts(parts) then
return false, 'Missing required parts'
end
RemovePartsFromInventory(parts)
-- Animate the repair
TaskTurnPedToFaceEntity(PlayerPedId(), vehicle, 1000)
Wait(1000)
if not StartProgressBar('Repairing engine...', 15000, 'mechanic_repair') then
return false, 'Repair cancelled'
end
SetVehicleEngineHealth(vehicle, 1000.0)
SetVehicleEngineOn(vehicle, true, true, false)
return true
end,
body_major = function(vehicle)
local parts = {'body_panel', 'filler_putty', 'paint_primer'}
if not HasRequiredParts(parts) then
return false, 'Missing required parts'
end
RemovePartsFromInventory(parts)
if not StartProgressBar('Repairing body damage...', 20000, 'mechanic_repair') then
return false, 'Repair cancelled'
end
SetVehicleBodyHealth(vehicle, 1000.0)
SetVehicleDeformationFixed(vehicle)
return true
end,
tire_replace = function(vehicle, tireIndex)
local parts = {'tire_set'}
if not HasRequiredParts(parts) then
return false, 'Missing required parts'
end
RemovePartsFromInventory(parts)
if not StartProgressBar('Replacing tire...', 8000, 'mechanic_repair') then
return false, 'Repair cancelled'
end
SetVehicleTyreFixed(vehicle, tireIndex)
return true
end
}
The parts system creates an additional economy layer on your server. Stock parts at NPC wholesalers or let players craft them at industrial locations. Mechanic shops can purchase parts in bulk at wholesale prices and store them in a shared shop inventory, which means the shop owner needs to manage their supply chain to keep the business running smoothly. This supply-and-demand dynamic adds tremendous depth to the roleplay experience.
Spray Painting and Cosmetic Services
Spray painting is one of the most visually satisfying services a mechanic shop can offer, and implementing it well requires working with several vehicle color native functions. FiveM provides natives for primary color, secondary color, pearlescent, wheel color, and custom RGB values. Build a color picker UI that lets the mechanic select from preset colors or enter custom RGB values for a premium price. The NUI should display a real-time preview by temporarily applying the color to the vehicle while the mechanic browses options. Here is how to handle the server-side color application after the mechanic confirms the paint job and the customer pays:
RegisterNetEvent('mechanic:server:applyPaint', function(netId, paintData, customerId)
local src = source
local Mechanic = QBCore.Functions.GetPlayer(src)
local Customer = QBCore.Functions.GetPlayer(customerId)
if not Mechanic or not Customer then return end
if Mechanic.PlayerData.job.name ~= 'mechanic' then return end
local gradeServices = Config.MechanicGrades[Mechanic.PlayerData.job.grade.level].services
if gradeServices ~= 'all' and not TableContains(gradeServices, 'spray_paint') then
return TriggerClientEvent('QBCore:Notify', src, 'Not authorized', 'error')
end
local price = paintData.isCustomRGB and Config.ServicePrices.spray_paint * 1.5
or Config.ServicePrices.spray_paint
if Customer.Functions.RemoveMoney('cash', price, 'mechanic-paint') then
local shopCut = math.floor(price * 0.6)
local mechanicCut = price - shopCut
Mechanic.Functions.AddMoney('cash', mechanicCut, 'mechanic-paint-commission')
AddToShopFunds(Mechanic.PlayerData.job.grade.level >= 3 and src, shopCut)
TriggerClientEvent('mechanic:client:applyPaint', customerId, netId, paintData)
TriggerClientEvent('QBCore:Notify', src, 'Paint applied - earned $' .. mechanicCut, 'success')
TriggerClientEvent('QBCore:Notify', customerId, 'Vehicle painted for $' .. price, 'success')
else
TriggerClientEvent('QBCore:Notify', src, 'Customer cannot afford this', 'error')
end
end)
Beyond basic paint, consider adding livery application, neon light installation, and window tinting as separate cosmetic services. Each service should have its own part requirements and price point. Neon lights, for example, require neon kits from the parts inventory and use the SetVehicleNeonLightEnabled and SetVehicleNeonLightsColour natives to apply the visual effect. Window tinting uses SetVehicleWindowTint with different tint levels that could be restricted by local server laws, giving police a reason to pull players over for illegal tints.
Performance Upgrades and Tuning
Performance tuning is where mechanics can really shine and earn top dollar on the server. FiveM provides extensive vehicle modification natives through the SetVehicleMod function, which accepts a mod type and mod index. The mod types cover engine upgrades (type 11), brakes (type 12), transmission (type 13), suspension (type 15), armor (type 16), and turbo (type 18). Each mod type has multiple levels that progressively improve the vehicle's performance. Build a tuning interface that shows available upgrades for the current vehicle, the cost of each level, and ideally a stat comparison showing the before and after values for speed, acceleration, braking, and handling:
function GetAvailableUpgrades(vehicle)
local upgrades = {}
local modTypes = {
{type = 11, label = 'Engine', icon = 'fa-engine'},
{type = 12, label = 'Brakes', icon = 'fa-brake'},
{type = 13, label = 'Transmission', icon = 'fa-gears'},
{type = 15, label = 'Suspension', icon = 'fa-car'},
{type = 16, label = 'Armor', icon = 'fa-shield'},
{type = 18, label = 'Turbo', icon = 'fa-bolt'}
}
SetVehicleModKit(vehicle, 0)
for _, mod in ipairs(modTypes) do
local currentLevel = GetVehicleMod(vehicle, mod.type)
local maxLevel = GetNumVehicleMods(vehicle, mod.type)
local isTurbo = mod.type == 18
table.insert(upgrades, {
type = mod.type,
label = mod.label,
icon = mod.icon,
currentLevel = currentLevel,
maxLevel = isTurbo and 1 or maxLevel,
installed = isTurbo and IsToggleModOn(vehicle, mod.type) or currentLevel >= 0,
price = CalculateUpgradePrice(mod.type, currentLevel + 1)
})
end
return upgrades
end
function ApplyPerformanceUpgrade(vehicle, modType, level)
SetVehicleModKit(vehicle, 0)
if modType == 18 then
ToggleVehicleMod(vehicle, 18, true)
else
SetVehicleMod(vehicle, modType, level, false)
end
end
Performance upgrades should require high-value parts like turbo kits, racing transmissions, or sport brake assemblies. These parts can be sourced from specialized suppliers or crafted by players with the right skills, creating cross-job dependencies that enrich the server economy. Always validate upgrade requests on the server to prevent clients from applying modifications they have not paid for or do not have the parts for.
Billing System and Revenue Sharing
A clean billing system ties the entire mechanic experience together and handles the financial transactions between the mechanic, the customer, and the shop. When a mechanic completes a service, generate an itemized bill that lists each service performed and its cost. Present the bill to the customer through a notification or NUI popup that they must accept before payment is processed. Split the revenue between the mechanic who performed the work and the shop's business account, with the split ratio configurable by the shop owner. Here is a server-side billing system that handles invoice creation and payment processing:
local activeInvoices = {}
RegisterNetEvent('mechanic:server:createInvoice', function(customerId, services)
local src = source
local Mechanic = QBCore.Functions.GetPlayer(src)
local Customer = QBCore.Functions.GetPlayer(customerId)
if not Mechanic or not Customer then return end
if Mechanic.PlayerData.job.name ~= 'mechanic' then return end
local totalPrice = 0
local lineItems = {}
for _, service in ipairs(services) do
local price = Config.ServicePrices[service.name]
if price then
totalPrice = totalPrice + price
table.insert(lineItems, {
name = service.label,
price = price
})
end
end
local invoiceId = 'INV-' .. os.time() .. '-' .. math.random(1000, 9999)
activeInvoices[invoiceId] = {
mechanic = src,
customer = customerId,
total = totalPrice,
items = lineItems,
shopId = Mechanic.PlayerData.metadata.currentShop,
timestamp = os.time()
}
TriggerClientEvent('mechanic:client:showInvoice', customerId, invoiceId, lineItems, totalPrice)
end)
RegisterNetEvent('mechanic:server:payInvoice', function(invoiceId)
local src = source
local invoice = activeInvoices[invoiceId]
if not invoice or invoice.customer ~= src then return end
local Customer = QBCore.Functions.GetPlayer(src)
local Mechanic = QBCore.Functions.GetPlayer(invoice.mechanic)
if Customer.Functions.RemoveMoney('cash', invoice.total, 'mechanic-invoice') then
local mechanicShare = math.floor(invoice.total * 0.4)
local shopShare = invoice.total - mechanicShare
if Mechanic then
Mechanic.Functions.AddMoney('cash', mechanicShare, 'mechanic-wage')
TriggerClientEvent('QBCore:Notify', invoice.mechanic,
'Received $' .. mechanicShare .. ' for services', 'success')
end
UpdateShopBalance(invoice.shopId, shopShare)
activeInvoices[invoiceId] = nil
TriggerClientEvent('QBCore:Notify', src, 'Paid $' .. invoice.total, 'success')
else
TriggerClientEvent('QBCore:Notify', src, 'Not enough cash', 'error')
end
end)
Consider supporting multiple payment methods including cash, bank transfers, and even cryptocurrency if your server has a crypto system. Track all transactions in a database log so shop owners can review their revenue history, identify their best-performing mechanics, and spot any suspicious billing patterns. This financial tracking also enables tax systems and government roleplay interactions where the state can audit mechanic shops.
Stash and Shop Inventory Management
Every mechanic shop needs a shared stash where parts are stored and accessible to all on-duty employees. Integrate with your server's inventory system, whether that is ox_inventory, qb-inventory, or a custom solution, to create a shop-specific stash that only authorized mechanics can access. The shop owner should have the ability to order parts from a wholesale supplier NPC, which deducts money from the shop's business account and adds the parts to the stash after a configurable delivery delay. This delay simulates real-world supply chain logistics and prevents instant restocking. Track stash access in a log so the shop owner can see which mechanics took which parts, preventing internal theft. On the client side, use a target interaction on a specific prop inside the shop, like a tool cabinet or parts shelf, to open the stash. Combine the stash with a crafting system where mechanics can assemble complex parts from raw materials, such as combining a turbo housing, impeller, and wastegate into a complete turbo kit, adding another skill-based layer to the job.
Tow Truck and Roadside Assistance
Extending the mechanic job to include tow truck and roadside assistance services greatly increases the job's activity and revenue potential. Mechanics should be able to check out a tow truck from their shop, drive to a stranded player's location, attach the broken vehicle using the AttachEntityToEntity native, and tow it back to the shop for repairs. Implement a dispatch system where players can request roadside assistance through their phone, which creates a blip on the map visible to all on-duty mechanics. The first mechanic to accept the call gets the job, and a GPS route is drawn to the customer's location. For the attachment system, calculate proper offset positions based on the tow truck model to ensure the towed vehicle sits correctly on the flatbed. Add a distance-based fee to the tow service so that longer tows cost the customer more, incentivizing mechanics to handle calls in their area while still allowing them to take lucrative long-distance jobs across the map.