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 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:
| Storage | What it contains |
|---|---|
| Garage vehicle column | GTA properties such as paint, wheels, performance mods, extras, neon, windows, and damage |
sky_mechanic_vehicle_tuning.properties | Mechanic-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
sky_mechanic_vehicle_tuning.properties with the garage column. Both storage
locations are required and have different purposes.Before you start
- Back up the framework client file.
- Back up the garage resource.
- Back up the owned-vehicle database table.
- Check whether your server uses QBCore or ESX.
- 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.
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:
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:
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:
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:
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:
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.
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:
- 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.
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
- 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 the framework getter still match the Sky getter contents?
- 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?
- Does the framework setter still match the Sky setter contents?
- 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.