Tutorial 2026-04-04

FiveM Garage System Development - Complete Guide

OntelMonke

OntelMonke

Admin & Developer at Agency Scripts

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.

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.