Browse docs

Garage Integration

Connect the central QBCore or ESX vehicle-property functions to Sky and fix tuning persistence in qb-garages.

What this guide solves

A garage saves vehicle tuning in four steps:

  1. The garage reads the properties of the live vehicle.
  2. The server stores the properties as JSON.
  3. The garage reads that JSON when the vehicle is spawned.
  4. The client applies the properties to the spawned vehicle.

Most garages already call the central QBCore or ESX functions for steps 1 and 4. The cleanest integration is therefore to make those two framework functions use exactly the same implementation as Sky. Compatible garages then receive the same property table without editing every getter and setter.

qb-garages still needs an additional fix: some versions try to save properties through a qb-mechanicjob event that does not exist when Sky Mechanic Job is installed. The complete fix for that is included below.

Two separate types of saved tuning

Sky Mechanic Job and the garage use separate storage locations:

StorageWhat it contains
Garage vehicle columnGTA properties such as paint, wheels, performance mods, extras, neon, windows, and damage
sky_mechanic_vehicle_tuning.propertiesMechanic-specific features such as Stancer, custom handling, Nitro, anti-lag, two-step, and Engine Sounds

The normal garage columns are usually:

  • QBCore: player_vehicles.mods
  • ESX: owned_vehicles.vehicle
Do not replace sky_mechanic_vehicle_tuning.properties with the garage column. Both storage locations are required and have different purposes.

Before you start

  1. Back up the framework client file.
  2. Back up the garage resource.
  3. Back up the owned-vehicle database table.
  4. Check whether your server uses QBCore or ESX.
  5. Complete only the matching framework section below.

The source of truth

The complete Sky implementation is stored in:

sky_base/config/vehicle_functions.lua

The two source functions are:

Functions.Vehicle.GetVehicleProperties(vehicle)
Functions.Vehicle.SetVehicleProperties(vehicle, props)

Whenever Sky adds or changes a property, synchronize the contents of these two functions with the matching framework functions.

The two property functions intentionally contain no Sky.*, QBCore.*, or ESX.* calls. They receive the raw vehicle entity, just like the QBCore and ESX functions. Their inner contents are therefore framework-neutral and can be copied unchanged without an adapter.

Do not copy only selected mod fields. The complete function contents must stay synchronized so paint, damage, wheels, tyres, windows, extras, neon, xenon, liveries, and framework-compatible aliases use the same schema.

QBCore: connect the central functions to Sky

Step 1: Open the correct file

Open:

qb-core/client/functions.lua

Search for:

function QBCore.Functions.GetVehicleProperties(vehicle)

The original function is long and reads every vehicle property with GTA natives.

Step 2: Synchronize the getter

Open these files next to each other:

Source: sky_base/config/vehicle_functions.lua
Target: qb-core/client/functions.lua

In the Sky file, find:

function Functions.Vehicle.GetVehicleProperties(vehicle)

Copy everything inside that function, but do not copy its first function ... line or its final closing end.

In QBCore, delete the old contents of:

function QBCore.Functions.GetVehicleProperties(vehicle)

Paste the copied Sky function contents directly into it. The resulting structure is:

qb-core/client/functions.lua
function QBCore.Functions.GetVehicleProperties(vehicle)
    -- Paste the complete contents of
    -- Functions.Vehicle.GetVehicleProperties(vehicle) here.
end

Both functions now use the same parameter name and the same raw vehicle entity.

Step 3: Synchronize the setter

In the same file, search for:

function QBCore.Functions.SetVehicleProperties(vehicle, props)

In sky_base/config/vehicle_functions.lua, find:

function Functions.Vehicle.SetVehicleProperties(vehicle, props)

Copy everything inside that function, excluding its first function ... line and its final closing end.

Delete the old contents of the QBCore setter and paste the Sky contents directly into it:

qb-core/client/functions.lua
function QBCore.Functions.SetVehicleProperties(vehicle, props)
    -- Paste the complete contents of
    -- Functions.Vehicle.SetVehicleProperties(vehicle, props) here.
end

Every compatible resource that calls these QBCore functions now uses the same property logic:

QBCore.Functions.GetVehicleProperties(vehicle)
QBCore.Functions.SetVehicleProperties(vehicle, props)

Step 4: Do not change the QBCore manifest

Do not add this:

dependency "sky_base"

to qb-core/fxmanifest.lua. Sky Base already needs QBCore, so adding the reverse dependency would create a circular manifest dependency.

ESX: connect the central functions to Sky

Step 1: Open the correct file

Open:

es_extended/client/functions.lua

Search for:

function ESX.Game.GetVehicleProperties(vehicle)

Step 2: Synchronize the getter

Copy the complete inner contents of:

function Functions.Vehicle.GetVehicleProperties(vehicle)

from sky_base/config/vehicle_functions.lua.

Delete the old contents of the ESX getter and paste the copied contents directly:

es_extended/client/functions.lua
function ESX.Game.GetVehicleProperties(vehicle)
    -- Paste the complete Sky getter contents here.
end

Step 3: Synchronize the setter

Search for:

function ESX.Game.SetVehicleProperties(vehicle, props)

Copy the complete inner contents of:

function Functions.Vehicle.SetVehicleProperties(vehicle, props)

Delete the old ESX setter contents, then use:

es_extended/client/functions.lua
function ESX.Game.SetVehicleProperties(vehicle, props)
    -- Paste the complete Sky setter contents here.
end

Step 4: Do not change the ESX manifest

Do not make es_extended depend on sky_base. Keep the normal start direction:

es_extended → sky_base → garage

Normal ESX garages that already use ESX.Game.GetVehicleProperties and ESX.Game.SetVehicleProperties now use Sky automatically.

QBCore: additional qb-garages fix

Changing the central QBCore getter and setter is not enough for affected qb-garages versions.

Some versions contain this call while parking:

TriggerServerEvent(
    "qb-mechanicjob:server:SaveVehicleProps",
    QBCore.Functions.GetVehicleProperties(vehicle)
)

That event belongs to a different Mechanic Job resource. Sky Mechanic Job does not provide it, so the event goes nowhere and player_vehicles.mods remains outdated.

The following steps make qb-garages save the JSON itself.

Step 1: Configure the property column

Open qb-garages/config.lua and add:

qb-garages/config.lua
Config.VehiclePropertiesStorage = {
    table = "player_vehicles",
    column = "mods",
    plateColumn = "plate",
    ownerColumn = "citizenid",
    ownerField = "citizenid"
}

Only change these values if your owned-vehicle table uses different names:

  • table: owned-vehicle table
  • column: JSON property column
  • plateColumn: plate column
  • ownerColumn: database owner column
  • ownerField: matching value in Player.PlayerData

Step 2: Read the properties before parking

Open qb-garages/client.lua and find:

local function DepositVehicle(veh, data)

Directly after the plate is read, add:

local plate = QBCore.Functions.GetPlate(veh)
local vehicle_properties = QBCore.Functions.GetVehicleProperties(veh)

Because the central QBCore getter was synchronized above, this call now produces the same property table as Sky.

Remove the complete old event:

TriggerServerEvent(
    "qb-mechanicjob:server:SaveVehicleProps",
    QBCore.Functions.GetVehicleProperties(veh)
)

Pass vehicle_properties as the final argument of the existing canDeposit callback:

QBCore.Functions.TriggerCallback(
    "qb-garages:server:canDeposit",
    function(can_deposit)
        -- Keep the existing garage code here.
    end,
    plate,
    data.type,
    data.indexgarage,
    1,
    vehicle_properties
)

Keep the existing fuel, damage, vehicle deletion, outside-vehicle, and notification code.

Step 3: Add the server-side storage helpers

Open qb-garages/server.lua. Add this block above the callback registrations:

qb-garages/server.lua
local function getVehiclePropertiesStorage(Player)
    local storage = Config.VehiclePropertiesStorage
    local identifiers = {
        storage.table,
        storage.column,
        storage.plateColumn,
        storage.ownerColumn,
        storage.ownerField
    }

    for _, identifier in ipairs(identifiers) do
        if type(identifier) ~= "string" or not identifier:match("^[%w_]+$") then
            error(("Invalid Config.VehiclePropertiesStorage identifier: %s"):format(tostring(identifier)))
        end
    end

    local owner = Player.PlayerData[storage.ownerField]
    if not owner then
        error(("PlayerData.%s is unavailable for vehicle property persistence"):format(storage.ownerField))
    end

    return storage, owner
end

local function saveVehicleProperties(Player, plate, properties)
    if type(properties) ~= "table" then return false end

    local storage, owner = getVehiclePropertiesStorage(Player)
    properties.plate = plate

    local query = ("UPDATE `%s` SET `%s` = ? WHERE `%s` = ? AND `%s` = ?"):format(
        storage.table,
        storage.column,
        storage.plateColumn,
        storage.ownerColumn
    )
    local affected_rows = MySQL.update.await(query, {
        json.encode(properties),
        plate,
        owner
    })

    if affected_rows > 0 then return true end

    -- MySQL can report zero affected rows when the stored JSON is already identical.
    local exists_query = ("SELECT 1 FROM `%s` WHERE `%s` = ? AND `%s` = ? LIMIT 1"):format(
        storage.table,
        storage.plateColumn,
        storage.ownerColumn
    )
    return MySQL.scalar.await(exists_query, { plate, owner }) ~= nil
end

local function loadVehicleProperties(Player, plate)
    local storage, owner = getVehiclePropertiesStorage(Player)
    local query = ("SELECT `%s` AS properties FROM `%s` WHERE `%s` = ? AND `%s` = ? LIMIT 1"):format(
        storage.column,
        storage.table,
        storage.plateColumn,
        storage.ownerColumn
    )
    local row = MySQL.single.await(query, { plate, owner })

    if not row or not row.properties then return {} end

    return json.decode(row.properties) or {}
end

The update checks both the plate and the owner. A player therefore cannot overwrite another player's vehicle by submitting its plate.

Step 4: Save inside canDeposit

Find:

QBCore.Functions.CreateCallback(
    "qb-garages:server:canDeposit",
    function(source, cb, plate, vehicle_type, garage, state)

Add properties as the final parameter:

QBCore.Functions.CreateCallback(
    "qb-garages:server:canDeposit",
    function(source, cb, plate, vehicle_type, garage, state, properties)

After ownership has been confirmed, but before the vehicle is marked as parked, add:

if not saveVehicleProperties(Player, plate, properties) then
    cb(false)
    return
end

The order must be:

  1. Confirm ownership.
  2. Save the property JSON.
  3. Mark the vehicle as parked.
  4. Return success to the client.
  5. Delete the live vehicle on the client.

Step 5: Load the configured property column

Find the server callback:

"qb-garages:server:spawnvehicle"

Get the QBCore player at the beginning:

local Player = exports["qb-core"]:GetPlayer(source)
if not Player then return end

Load the configured property column:

local vehicle_properties = loadVehicleProperties(Player, plate)

Return it with the spawned network ID and plate:

cb(net_id, vehicle_properties, plate)

Step 6: Keep the normal QBCore setter

When the client receives the spawned vehicle, it should use:

QBCore.Functions.SetVehicleProperties(vehicle, props)

Do not replace this call in every garage. The central QBCore setter already contains the same implementation as Sky.

Keep the garage's existing fuel, keys, engine state, visual damage, warp, and outside-vehicle code.

ESX garages

After changing the two central ESX functions, most ESX garages need no client modification.

Confirm that the garage:

  1. Uses ESX.Game.GetVehicleProperties while parking.
  2. Encodes the complete returned table with json.encode.
  3. Stores it in owned_vehicles.vehicle.
  4. Reads and decodes the same column while spawning.
  5. Uses ESX.Game.SetVehicleProperties after the vehicle exists.

If all five points are already true, no garage code needs to be changed.

Custom garages that bypass the framework

Some garages use their own adapter functions instead of the central framework functions. Common names are:

CL.GetVehicleProperties
CL.SetVehicleProperties

When possible, make the adapter call the synchronized framework functions:

CL.GetVehicleProperties = function(vehicle)
    return QBCore.Functions.GetVehicleProperties(vehicle)
end

CL.SetVehicleProperties = function(vehicle, props)
    QBCore.Functions.SetVehicleProperties(vehicle, props)
end

For an ESX garage, use ESX.Game.GetVehicleProperties and ESX.Game.SetVehicleProperties instead. If the adapter supports both frameworks, select the matching pair in its existing framework branch.

Updates and maintenance

Framework and Sky updates can make the copies different again. After updating QBCore, ESX, or Sky:

  1. Open sky_base/config/vehicle_functions.lua.
  2. Open the framework client functions file next to it.
  3. Compare both getter bodies.
  4. Compare both setter bodies.
  5. Copy the Sky contents again when anything differs.

No self adapter is needed. Sky, QBCore, and ESX all pass the raw vehicle entity as vehicle.

The actual property implementation remains configurable in:

sky_base/config/vehicle_functions.lua

Step-by-step test

Use one owned test vehicle:

  1. Start the framework, sky_base, the garage, and sky_mechanicjob.
  2. Take the vehicle out.
  3. Change an obvious value such as paint, wheels, or an engine upgrade.
  4. Park the vehicle.
  5. Check the database:
    • QBCore: player_vehicles.mods
    • ESX: owned_vehicles.vehicle
  6. Confirm that the JSON contains the plate and changed mod values.
  7. Take the vehicle out again.
  8. Confirm that the tuning was restored.
  9. Restart the server.
  10. Retrieve the vehicle and verify it again.

Then test:

  • custom and standard paint
  • wheels and custom tyres
  • performance upgrades
  • extras
  • neon and xenon
  • windows, doors, and tyre state
  • livery
  • Stancer
  • Nitro, anti-lag, and two-step
  • Engine Sound

If tuning is still missing

Check these points in order:

  1. Does the framework getter still match the Sky getter contents?
  2. Does the garage JSON column change while parking?
  3. Is the database update restricted by plate and owner?
  4. Does the spawn callback read the same JSON column?
  5. Does the framework setter still match the Sky setter contents?
  6. Does sky_base start after the framework and before the garage?
  7. Does sky_mechanic_vehicle_tuning contain a row for the exact plate?
  8. Does another script apply old properties after the framework setter?

If normal paint and wheels are missing, inspect the framework and garage property flow. If only Stancer, Nitro, anti-lag, or Engine Sound is missing, inspect sky_mechanic_vehicle_tuning.properties and the exact plate value.