Tutorial 2026-04-06

Building a Mechanic Job System for FiveM

TDYSKY

TDYSKY

Founder & Lead Developer at Agency Scripts

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.

Share this article

Ready to upgrade your server?

Check out our premium FiveM scripts in the Agency Scripts store or join our Discord community for support and updates.