Browse docs
Garage Integration
What this guide solves
A garage saves vehicle tuning in four steps:
- The garage reads the properties of the live vehicle.
- The server stores the properties as JSON.
- The garage reads that JSON when the vehicle is spawned.
- The client applies the properties to the spawned vehicle.
Most QBCore and ESX garages already call the central framework functions for steps 1 and 4. The
cleanest integration is therefore to synchronize those two functions with Sky. Qbox uses its
supported Ox property and qbx_vehicles pipeline instead; do not patch Qbox core files.
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 persistence records
Sky Mechanic Job and the garage use separate storage locations:
| Storage | What it contains |
|---|---|
| Garage vehicle column | The framework's owned-vehicle snapshot for paint, wheels, performance mods, extras, neon, windows, damage, and other GTA properties |
sky_mechanic_vehicle_tuning.properties | A compatibility snapshot of GTA properties plus Mechanic-specific _skyMechanicTuning and _skyMechanicPaint extensions 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 - Qbox:
player_vehicles.mods, managed throughqbx_vehicles
The two snapshots overlap in the current implementation. Keep both: the garage record owns the normal owned-vehicle spawn, while Mechanic Job reapplies its persisted tuning after the vehicle is available. If their paint, mods, neon, or xenon values differ, the restore that runs last can overwrite the other. Compare both JSON payloads and the apply order when only some tuning changes after a garage cycle.
sky_mechanic_vehicle_tuning.properties with the garage column. Both storage
locations are required and have different purposes.Before you start
- For QBCore or ESX, back up the framework client file.
- Back up the garage resource.
- Back up the owned-vehicle database table.
- Check whether your server uses QBCore, ESX, or Qbox.
- Complete only the matching framework section below.
QBCore and ESX 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.
Most of the property code uses GTA natives and can be shared unchanged. Fuel is the intentional
exception: inside sky_base, the getter and setter call Sky.Functions.GetVehicleFuel and
Sky.Functions.SetVehicleFuel so the configured Sky fuel provider is used. Those two calls are
valid inside sky_base, but they must not be copied into qb-core or es_extended. FiveM Lua
globals are resource-local, and those framework resources do not load the Sky bootstrap. A manifest
dependency can control start order, but it does not share the Sky global between resources.
Required fuel substitutions
After copying the Sky getter body into a framework function, replace:
fuelLevel = round(Sky.Functions.GetVehicleFuel(vehicle), 1),
with the framework-local native version:
fuelLevel = round(GetVehicleFuelLevel(vehicle), 1),
After copying the Sky setter body, replace:
Sky.Functions.SetVehicleFuel(vehicle, props.fuelLevel)
with:
SetVehicleFuelLevel(vehicle, props.fuelLevel + 0.0)
If the framework functions already use a fuel-resource export, preserve those original two fuel calls instead of the native examples. The target function must use only APIs available inside its own framework resource.
Sky.* call in qb-core/client/functions.lua or
es_extended/client/functions.lua. An F8 error such as
attempt to index a nil value (global 'Sky') means the fuel substitution was skipped. The getter
then returns no property table, while the setter stops before paint, xenon, and the remaining mods
are applied.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. Then apply the required fuel substitution before
restarting the resource.
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:
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. Then apply the required fuel substitution.
Delete the old contents of the QBCore setter and paste the Sky contents directly into it:
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 schema, while QBCore keeps its own fuel integration:
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.
Apply the required fuel substitution immediately after pasting the getter body.
Delete the old contents of the ESX getter and paste the copied contents directly:
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, apply the required fuel substitution, then use:
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 the synchronized property schema automatically while ESX
keeps its own fuel integration.
Qbox: keep the supported property pipeline
Do not paste either Sky vehicle-property function into qbx_core, its QBCore bridge,
qbx_garages, or ox_lib. Qbox explicitly recommends leaving core code unchanged.
Current qbx_garages captures the vehicle with lib.getVehicleProperties(vehicle), stores that
property table through qbx_vehicles:SaveVehicle, and passes the stored properties into the
owned-vehicle spawn flow.
Keep these Qbox/Ox APIs in their existing roles. Capture the properties on the client:
local properties = lib.getVehicleProperties(vehicle)
Pass that table to the server through the garage's existing callback. Store it on the server:
exports.qbx_vehicles:SaveVehicle(vehicle, {
props = properties
})
When a custom server-side spawn flow creates the owned vehicle, pass the stored properties during spawn:
qbx.spawnVehicle({
-- Keep the garage's model, spawn source, and other options.
props = properties
})
The standard current qbx_garages already follows this flow and needs no Sky code copied into it.
A custom Qbox garage should integrate with these supported APIs instead of editing qbx_core. No
Sky.* call belongs in a Qbox core resource.
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:
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 tablecolumn: JSON property columnplateColumn: plate columnownerColumn: database owner columnownerField: matching value inPlayer.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:
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:
- Confirm ownership.
- Save the property JSON.
- Mark the vehicle as parked.
- Return success to the client.
- 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:
- Uses
ESX.Game.GetVehiclePropertieswhile parking. - Encodes the complete returned table with
json.encode. - Stores it in
owned_vehicles.vehicle. - Reads and decodes the same column while spawning.
- Uses
ESX.Game.SetVehiclePropertiesafter 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.
For Qbox, keep the Ox/qbx_vehicles flow described above. If the adapter supports multiple
frameworks, select the matching path in its existing framework branch.
Updates and maintenance
Framework and Sky updates can make the copies different again. After updating QBCore, ESX, or Sky:
- Open
sky_base/config/vehicle_functions.lua. - Open the framework client functions file next to it.
- Compare both getter bodies.
- Compare both setter bodies.
- Copy the Sky contents again when anything differs.
- Reapply the two framework-local fuel substitutions.
- Search the framework target file for
Sky.and confirm that no matches remain.
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:
- Start the framework,
sky_base, the garage, andsky_mechanicjob. - Take the vehicle out.
- Change an obvious value such as paint, wheels, or an engine upgrade.
- Park the vehicle.
- Check the database:
- QBCore:
player_vehicles.mods - ESX:
owned_vehicles.vehicle - Qbox: the
player_vehiclesproperty record managed byqbx_vehicles
- QBCore:
- Confirm that the JSON contains the plate and changed mod values.
- Take the vehicle out again.
- Confirm that the tuning was restored.
- Restart the server.
- 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:
- Does F8 show
attempt to index a nil value (global 'Sky')from a framework property function? - Is there any
Sky.call left in the framework getter or setter? - On QBCore or ESX, does the framework getter still match the Sky getter contents apart from the
local fuel call? On Qbox, does the garage still call
lib.getVehicleProperties? - Does the garage JSON column change while parking?
- Is the database update restricted by plate and owner?
- Does the spawn callback read the same JSON column?
- On QBCore or ESX, does the framework setter still match the Sky setter contents apart from the local fuel call? On Qbox, are the stored properties passed into the owned-vehicle spawn flow?
- Does
sky_basestart after the framework and before the garage? - Does
sky_mechanic_vehicle_tuningcontain a row for the exact plate? - 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.