Understanding Garage System Architecture
A garage system is one of the most essential features in any FiveM roleplay server, serving as the primary way players store, retrieve, and manage their vehicles. At its core, a garage system consists of three interconnected layers: a database layer that persists vehicle ownership and state, a server-side logic layer that handles spawn and despawn operations with validation, and a client-side UI layer that lets players interact with their stored vehicles. Before writing any code, you need to decide on key architectural choices like whether garages are location-based or global, whether players can access any garage or only specific ones, and how you want to handle vehicle properties like modifications, fuel level, and damage state. The best garage systems store the complete vehicle properties object so that when a player retrieves their car, it comes back exactly as they left it, including custom paint jobs, performance upgrades, and even the dirt level on the body.
Database Schema and Vehicle Persistence
Your database schema forms the foundation of the entire garage system. You need a table that tracks vehicle ownership, current state, and stored properties. The state column is critical because it determines whether a vehicle is currently spawned in the world, stored in a garage, or sitting in the impound lot. Here is a practical schema that covers the essential fields:
CREATE TABLE IF NOT EXISTS player_vehicles (
id INT AUTO_INCREMENT PRIMARY KEY,
citizenid VARCHAR(50) NOT NULL,
vehicle VARCHAR(50) NOT NULL,
hash VARCHAR(50) NOT NULL,
mods LONGTEXT DEFAULT '{}',
plate VARCHAR(8) NOT NULL,
fakeplate VARCHAR(8) DEFAULT NULL,
garage VARCHAR(50) DEFAULT 'pillboxgarage',
fuel INT DEFAULT 100,
engine FLOAT DEFAULT 1000.0,
body FLOAT DEFAULT 1000.0,
state INT DEFAULT 1, -- 0 = out, 1 = garaged, 2 = impounded
depotprice INT DEFAULT 0,
drivingdistance INT DEFAULT 0,
INDEX idx_citizenid (citizenid),
INDEX idx_plate (plate),
INDEX idx_state (state)
);
The mods column stores a JSON-encoded object containing all vehicle modifications returned by functions like QBCore.Functions.GetVehicleProperties(vehicle) or the equivalent in ESX. Indexing the citizenid, plate, and state columns ensures that lookups remain fast even as your player base grows into the thousands. Always use parameterized queries when interacting with this table to prevent SQL injection attacks.
Server-Side Spawn and Despawn Logic
The server side is where all critical validation happens. When a player requests to take a vehicle out of the garage, the server must verify that the player actually owns that vehicle, the vehicle is currently in the garaged state, and there is a valid spawn point available. Never let the client dictate the spawn position directly because cheaters could spawn vehicles anywhere on the map. Instead, define spawn points on the server and select the nearest available one. Here is an example of a secure server-side takeout handler:
RegisterNetEvent('garage:server:takeVehicle', function(vehicleId, garageId)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local citizenid = Player.PlayerData.citizenid
local result = MySQL.query.await(
'SELECT * FROM player_vehicles WHERE id = ? AND citizenid = ? AND state = 1',
{vehicleId, citizenid}
)
if not result or not result[1] then
TriggerClientEvent('QBCore:Notify', src, 'Vehicle not found', 'error')
return
end
local vehData = result[1]
local spawnPoint = GetAvailableSpawnPoint(garageId)
if not spawnPoint then
TriggerClientEvent('QBCore:Notify', src, 'No parking spots available', 'error')
return
end
MySQL.update('UPDATE player_vehicles SET state = 0 WHERE id = ?', {vehicleId})
TriggerClientEvent('garage:client:spawnVehicle', src, vehData, spawnPoint)
end)
For the despawn process, the server needs to capture the current vehicle properties before removing it from the world. This ensures modifications made since the last storage are saved. Always update fuel, engine health, and body health alongside the mods JSON so that everything persists correctly. Implement a distance check on the server side to make sure the player is actually near a garage location before allowing storage operations.
Client-Side Garage UI with NUI
The garage UI is where players interact with the system, and a well-designed interface makes the difference between a frustrating experience and a seamless one. Use NUI with HTML, CSS, and JavaScript to build a responsive panel that displays all vehicles stored in the current garage. Each vehicle entry should show the vehicle name, license plate, fuel level, and overall condition at a glance. Include a preview system that spawns the vehicle model temporarily so players can see what they are selecting, especially useful when a player owns multiple vehicles of the same type. Here is the client-side logic for opening the garage menu and collecting vehicle data:
RegisterNetEvent('garage:client:openMenu', function(garageId)
QBCore.Functions.TriggerCallback('garage:server:getVehicles', function(vehicles)
if not vehicles or #vehicles == 0 then
QBCore.Functions.Notify('No vehicles stored here', 'info')
return
end
SetNuiFocus(true, true)
SendNUIMessage({
action = 'openGarage',
vehicles = vehicles,
garageName = Config.Garages[garageId].label
})
end, garageId)
end)
RegisterNUICallback('takeVehicle', function(data, cb)
SetNuiFocus(false, false)
TriggerServerEvent('garage:server:takeVehicle', data.vehicleId, currentGarage)
cb('ok')
end)
On the JavaScript side, render each vehicle as a card with action buttons for taking the vehicle out or transferring it to another garage. Consider adding sorting and filtering options so players with large collections can quickly find the vehicle they need. A search bar that filters by plate number or vehicle name is a small addition that dramatically improves usability on servers where players accumulate many vehicles over time.
Vehicle Property Storage and Restoration
Properly saving and restoring vehicle properties is one of the trickiest parts of garage system development. The properties object contains dozens of fields including colors, liveries, neon lights, window tints, tire smoke color, extras, and every performance modification. When storing a vehicle, capture the properties immediately before deleting the entity to ensure you get the most current state. When spawning a vehicle back, you need to wait for the entity to fully load before applying properties, otherwise modifications like custom wheels or engine upgrades will silently fail. Use a small delay or a proper entity existence check loop:
function SpawnAndApplyMods(vehData, spawnPoint)
local model = GetHashKey(vehData.vehicle)
RequestModel(model)
while not HasModelLoaded(model) do
Wait(10)
end
local veh = CreateVehicle(model, spawnPoint.x, spawnPoint.y, spawnPoint.z,
spawnPoint.w, true, false)
while not DoesEntityExist(veh) do
Wait(10)
end
local props = json.decode(vehData.mods)
if props then
QBCore.Functions.SetVehicleProperties(veh, props)
end
SetVehicleFuelLevel(veh, vehData.fuel + 0.0)
SetVehicleEngineHealth(veh, vehData.engine + 0.0)
SetVehicleBodyHealth(veh, vehData.body + 0.0)
SetEntityAsMissionEntity(veh, true, true)
SetModelAsNoLongerNeeded(model)
TaskWarpPedIntoVehicle(PlayerPedId(), veh, -1)
end
Pay special attention to addon vehicles because they sometimes have custom extras or livery indices that behave differently from vanilla GTA vehicles. Test your property save and restore cycle thoroughly with a variety of vehicle types to catch edge cases early.
Impound System Integration
An impound system works hand-in-hand with your garage and adds a layer of realism that roleplay servers demand. Vehicles end up in the impound for several reasons: police seizure during an arrest, automatic cleanup of abandoned vehicles after a server restart, or admin action for rule violations. When a vehicle is impounded, update its state to 2 in the database and optionally set a depot price that the player must pay to retrieve it. The impound lot should function similarly to a garage but with the added requirement of payment before release. Create a separate impound location on the map with its own spawn points and NUI interface that displays the impound fee prominently.
RegisterNetEvent('police:server:impoundVehicle', function(plate, price)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
-- Verify the player has police job authorization
if Player.PlayerData.job.name ~= 'police' then return end
local result = MySQL.update.await(
'UPDATE player_vehicles SET state = 2, depotprice = ? WHERE plate = ?',
{price or 500, plate}
)
if result > 0 then
TriggerClientEvent('QBCore:Notify', src, 'Vehicle impounded', 'success')
end
end)
Consider implementing a tiered pricing system where the impound fee increases each time the same vehicle is impounded, discouraging players from treating impound as free parking. You can also add a time-based mechanic where vehicles left in impound for more than a configurable number of real-world days are automatically released back to the garage at no charge, preventing permanent loss scenarios that frustrate players.
Garage Blips and Target Integration
Making garages discoverable and easy to interact with requires proper blip placement and interaction zones. Add map blips for each garage location so players can find them on the minimap, and use either proximity-based markers or target system integration for the interaction trigger. Target systems like ox_target or qb-target provide a cleaner experience because they only show interaction options when the player aims at a specific point, reducing screen clutter. Define your garage locations in a shared config file that both the client and server can reference, keeping coordinates, spawn points, and settings synchronized:
Config.Garages = {
['pillboxgarage'] = {
label = 'Pillbox Garage',
coords = vector3(215.83, -810.18, 30.73),
spawnPoints = {
vector4(218.32, -803.28, 30.73, 248.5),
vector4(222.41, -799.84, 30.73, 248.5),
vector4(226.52, -796.41, 30.73, 248.5),
},
blip = { sprite = 357, color = 3, scale = 0.7 },
vehicleType = 'car', -- car, boat, aircraft
},
}
Support multiple vehicle types by creating separate garages for boats and aircraft with appropriate spawn locations near water or at airports. The vehicleType filter ensures that players only see land vehicles at a street garage and only see boats at a marina, preventing confusion and spawn issues. When a player approaches a garage, check whether they have any vehicles stored there before showing the interaction prompt to avoid unnecessary menu opens for players who have no vehicles at that location.
Performance Optimization Tips
Garage systems can become a performance bottleneck if not implemented carefully, especially on servers with hundreds of concurrent players each owning multiple vehicles. Cache vehicle lists on the server side instead of querying the database every time a player opens a garage menu, and invalidate the cache only when a vehicle state changes. On the client side, avoid keeping NUI frames open when not needed because even hidden NUI frames consume resources if they are running JavaScript timers or animations. When spawning vehicles, ensure you are properly cleaning up entities by setting them as no longer needed after the player stores them, and implement a fallback cleanup routine that runs periodically to catch any orphaned vehicle entities that were not properly despawned due to crashes or disconnections. Use native functions like GetGamePool('CVehicle') sparingly and cache the results when you need to check for existing player vehicles in the world. Finally, consider implementing a maximum vehicle limit per garage to keep database queries bounded and prevent any single player from storing hundreds of vehicles that could slow down retrieval operations.