docsresourcesQb Garages

QB-Garages

QB-Garages provides a comprehensive vehicle storage and management system for QBCore servers, offering players secure parking, vehicle customization, and advanced garage management features.

📋 Overview

QB-Garages includes:

  • Multiple Garage Types - Public, private, job-specific, and gang garages
  • Vehicle Storage - Secure parking with damage persistence
  • Vehicle Management - Track vehicle status, location, and condition
  • Parking System - Realistic parking mechanics with designated spots
  • Impound Integration - Vehicle impounding and retrieval system
  • Garage Ownership - Purchase and manage private garage spaces
  • Vehicle Sharing - Share vehicle access with other players
  • Advanced Security - Anti-theft and access control systems

⚙️ Installation & Setup

Prerequisites

  • QB-Core framework installed
  • QB-Inventory for vehicle trunk storage
  • QB-Banking for garage purchases and fees
  • QB-Fuel (recommended for fuel persistence)
  • QB-VehicleKeys for vehicle access control

Installation Steps

  1. Download Resource

    git clone https://github.com/qbcore-framework/qb-garages.git
  2. Place in Resources

    resources/
    └── [qb]/
        └── qb-garages/
  3. Database Setup

    -- Import the SQL file
    SOURCE qb-garages.sql;
     
    -- Player vehicles table (if not exists)
    CREATE TABLE IF NOT EXISTS `player_vehicles` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `license` varchar(50) DEFAULT NULL,
      `citizenid` varchar(50) DEFAULT NULL,
      `vehicle` varchar(50) DEFAULT NULL,
      `hash` varchar(50) DEFAULT NULL,
      `mods` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
      `plate` varchar(50) NOT NULL,
      `fakeplate` varchar(50) DEFAULT NULL,
      `garage` varchar(50) DEFAULT NULL,
      `fuel` int(11) DEFAULT 100,
      `engine` float DEFAULT 1000,
      `body` float DEFAULT 1000,
      `state` int(11) DEFAULT 1,
      `depotprice` int(11) NOT NULL DEFAULT 0,
      `drivingdistance` int(50) DEFAULT NULL,
      `status` text DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `plate` (`plate`),
      KEY `citizenid` (`citizenid`),
      KEY `license` (`license`)
    );
     
    -- Garage ownership table
    CREATE TABLE IF NOT EXISTS `garage_ownership` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `citizenid` varchar(50) NOT NULL,
      `garage_id` varchar(50) NOT NULL,
      `purchase_date` timestamp DEFAULT CURRENT_TIMESTAMP,
      `access_list` text DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `citizenid` (`citizenid`),
      KEY `garage_id` (`garage_id`)
    );
  4. Server Configuration

    # server.cfg
    ensure qb-garages

🔧 Configuration

Main Configuration

-- config.lua
Config = Config or {}
 
-- Garage settings
Config.UsingTarget = true -- Use qb-target for interactions
Config.UseRealisticParking = true -- Vehicles stay where parked
Config.AutoRespawn = true -- Auto-respawn vehicles on server restart
Config.SharedGarages = true -- Allow multiple players in same garage
 
-- Vehicle spawn settings
Config.SpawnDistance = 4.0 -- Distance from garage to spawn vehicles
Config.MinimumDistanceSpawn = 3.0 -- Minimum distance between spawned vehicles
 
-- Garage fees
Config.DepotPrice = 250 -- Impound retrieval fee
Config.ParkingMeter = true -- Enable parking meter fees
Config.ParkingCost = 10 -- Cost per hour for public parking
 
-- Damage system
Config.DamageMultiplier = 1.0 -- Damage persistence multiplier
Config.FixEnginePercent = 25 -- Minimum engine health to drive
Config.FixBodyPercent = 25 -- Minimum body health threshold

Garage Locations

-- config.lua - Garage configurations
Config.Garages = {
    ["legion"] = {
        label = "Legion Square Garage",
        type = "public", -- public, private, job, gang
        coords = vector3(215.9, -810.1, 30.7),
        spawnPoint = vector4(229.7, -800.1, 30.6, 157.5),
        putVehicle = vector3(229.7, -800.1, 30.6),
        isOpened = true,
        showBlip = true,
        blipcoords = vector3(215.9, -810.1, 30.7),
        blipName = "Public Garage",
        blipNumber = 357,
        blipColor = 3,
        job = nil, -- Job requirement (if job garage)
        gang = nil, -- Gang requirement (if gang garage)
        vehicleCategories = {"car", "motorcycle"}, -- Allowed vehicle types
        maxVehicles = 50, -- Maximum vehicles that can be stored
        hourlyRate = 5, -- Hourly parking fee (if applicable)
        parkingSpots = {
            vector4(215.0, -805.0, 30.6, 157.5),
            vector4(218.0, -803.0, 30.6, 157.5),
            vector4(221.0, -801.0, 30.6, 157.5),
            vector4(224.0, -799.0, 30.6, 157.5)
        }
    },
    
    ["airport"] = {
        label = "Los Santos International",
        type = "public",
        coords = vector3(-796.6, -2025.1, 8.9),
        spawnPoint = vector4(-800.0, -2016.0, 8.9, 48.0),
        putVehicle = vector3(-800.0, -2016.0, 8.9),
        isOpened = true,
        showBlip = true,
        blipcoords = vector3(-796.6, -2025.1, 8.9),
        blipName = "Airport Garage",
        blipNumber = 357,
        blipColor = 3,
        vehicleCategories = {"car", "motorcycle", "plane", "helicopter"},
        maxVehicles = 100,
        parkingSpots = {
            vector4(-796.0, -2020.0, 8.9, 48.0),
            vector4(-799.0, -2018.0, 8.9, 48.0),
            vector4(-802.0, -2016.0, 8.9, 48.0)
        }
    },
    
    ["police"] = {
        label = "LSPD Garage",
        type = "job",
        coords = vector3(454.6, -1017.4, 28.4),
        spawnPoint = vector4(438.4, -1018.3, 27.7, 90.0),
        putVehicle = vector3(438.4, -1018.3, 27.7),
        isOpened = true,
        showBlip = false,
        job = "police",
        vehicleCategories = {"emergency"},
        maxVehicles = 20
    },
    
    ["ballas"] = {
        label = "Ballas Gang Garage",
        type = "gang",
        coords = vector3(102.3, -1956.8, 20.7),
        spawnPoint = vector4(109.8, -1961.4, 20.6, 320.0),
        putVehicle = vector3(109.8, -1961.4, 20.6),
        isOpened = true,
        showBlip = false,
        gang = "ballas",
        vehicleCategories = {"car", "motorcycle"},
        maxVehicles = 15
    }
}

Vehicle Categories

-- Vehicle type definitions
Config.VehicleCategories = {
    ["car"] = {
        label = "Cars",
        classes = {0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 17, 18, 19, 20}
    },
    ["motorcycle"] = {
        label = "Motorcycles", 
        classes = {8}
    },
    ["emergency"] = {
        label = "Emergency Vehicles",
        classes = {18}
    },
    ["plane"] = {
        label = "Planes",
        classes = {16}
    },
    ["helicopter"] = {
        label = "Helicopters", 
        classes = {15}
    },
    ["boat"] = {
        label = "Boats",
        classes = {14}
    },
    ["bicycle"] = {
        label = "Bicycles",
        classes = {13}
    }
}

🚗 Vehicle Management

Storing Vehicles

-- Store vehicle in garage
RegisterNetEvent('qb-garages:server:updateVehicleState', function(state, plate, garage)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    
    if not Player then return end
    
    MySQL.update('UPDATE player_vehicles SET state = ?, garage = ? WHERE plate = ? AND citizenid = ?', {
        state, garage, plate, Player.PlayerData.citizenid
    }, function(affectedRows)
        if affectedRows > 0 then
            TriggerClientEvent('QBCore:Notify', src, 'Vehicle stored successfully', 'success')
        end
    end)
end)

Vehicle Status System

-- Vehicle state definitions
Config.VehicleStates = {
    [0] = "Out", -- Vehicle is currently spawned/being used
    [1] = "Garaged", -- Vehicle is stored in garage
    [2] = "Impounded", -- Vehicle has been impounded
    [3] = "Destroyed", -- Vehicle was destroyed and needs insurance claim
    [4] = "Stolen" -- Vehicle was reported stolen
}
 
-- Get vehicle status with details
local function GetVehicleStatus(plate)
    local result = MySQL.Sync.fetchSingle('SELECT * FROM player_vehicles WHERE plate = ?', {plate})
    
    if result then
        return {
            state = result.state,
            garage = result.garage,
            fuel = result.fuel,
            engine = result.engine,
            body = result.body,
            mods = json.decode(result.mods),
            location = result.garage or "Unknown"
        }
    end
    
    return nil
end

🔌 API Reference

Server Events

Vehicle Spawning

-- Spawn vehicle from garage
RegisterNetEvent('qb-garages:server:spawnvehicle', function(plate, garage, coords)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    
    if not Player then return end
    
    -- Get vehicle data
    local result = MySQL.Sync.fetchSingle(
        'SELECT * FROM player_vehicles WHERE plate = ? AND citizenid = ?', 
        {plate, Player.PlayerData.citizenid}
    )
    
    if result and result.state == 1 then -- Vehicle is garaged
        -- Check garage capacity
        if IsGarageFull(garage) then
            TriggerClientEvent('QBCore:Notify', src, 'Garage is full', 'error')
            return
        end
        
        -- Update vehicle state to out
        MySQL.update('UPDATE player_vehicles SET state = 0 WHERE plate = ?', {plate})
        
        -- Spawn vehicle
        TriggerClientEvent('qb-garages:client:doSpawnVehicle', src, {
            model = result.vehicle,
            plate = result.plate,
            mods = json.decode(result.mods),
            fuel = result.fuel,
            engine = result.engine,
            body = result.body,
            coords = coords
        })
    end
end)
 
-- Store vehicle in garage
RegisterNetEvent('qb-garages:server:storeVehicle', function(plate, garage, vehProps)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    
    if not Player then return end
    
    -- Save vehicle modifications and state
    MySQL.update([[
        UPDATE player_vehicles 
        SET state = 1, garage = ?, mods = ?, fuel = ?, engine = ?, body = ?
        WHERE plate = ? AND citizenid = ?
    ]], {
        garage,
        json.encode(vehProps.mods),
        vehProps.fuel,
        vehProps.engine,
        vehProps.body,
        plate,
        Player.PlayerData.citizenid
    })
    
    TriggerClientEvent('QBCore:Notify', src, 'Vehicle stored in ' .. garage, 'success')
end)

Impound System

-- Impound vehicle
RegisterNetEvent('qb-garages:server:impoundVehicle', function(plate, reason, fee)
    local src = source
    
    -- Update vehicle state to impounded
    MySQL.update('UPDATE player_vehicles SET state = 2, depotprice = ? WHERE plate = ?', {
        fee or Config.DepotPrice, plate
    })
    
    -- Log impound reason
    MySQL.insert('INSERT INTO vehicle_impounds (plate, reason, fee, impound_date) VALUES (?, ?, ?, ?)', {
        plate, reason, fee, os.date('%Y-%m-%d %H:%M:%S')
    })
    
    TriggerClientEvent('QBCore:Notify', src, 'Vehicle has been impounded', 'error')
end)
 
-- Release vehicle from impound
RegisterNetEvent('qb-garages:server:payDepot', function(plate)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    
    if not Player then return end
    
    local result = MySQL.Sync.fetchSingle('SELECT depotprice FROM player_vehicles WHERE plate = ? AND citizenid = ?', {
        plate, Player.PlayerData.citizenid
    })
    
    if result and result.depotprice > 0 then
        if Player.Functions.RemoveMoney('bank', result.depotprice) then
            MySQL.update('UPDATE player_vehicles SET state = 1, depotprice = 0, garage = ? WHERE plate = ?', {
                'impoundlot', plate
            })
            TriggerClientEvent('QBCore:Notify', src, 'Vehicle released from impound', 'success')
        else
            TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
        end
    end
end)

Client Events

Garage Interface

-- Open garage menu
RegisterNetEvent('qb-garages:client:openGarage', function(garageId)
    local garage = Config.Garages[garageId]
    if not garage then return end
    
    -- Check access permissions
    if not HasGarageAccess(garageId) then
        QBCore.Functions.Notify('You do not have access to this garage', 'error')
        return
    end
    
    -- Get player's vehicles in this garage
    QBCore.Functions.TriggerCallback('qb-garages:server:getGarageVehicles', function(vehicles)
        if #vehicles > 0 then
            SetNuiFocus(true, true)
            SendNUIMessage({
                action = "openGarage",
                garage = garage,
                vehicles = vehicles
            })
        else
            QBCore.Functions.Notify('No vehicles in this garage', 'primary')
        end
    end, garageId)
end)
 
-- Vehicle spawning
RegisterNetEvent('qb-garages:client:doSpawnVehicle', function(vehicleData)
    local model = GetHashKey(vehicleData.model)
    
    -- Request model
    RequestModel(model)
    while not HasModelLoaded(model) do
        Wait(10)
    end
    
    -- Create vehicle
    local vehicle = CreateVehicle(model, vehicleData.coords.x, vehicleData.coords.y, vehicleData.coords.z, vehicleData.coords.w, true, false)
    
    -- Apply vehicle properties
    SetVehicleNumberPlateText(vehicle, vehicleData.plate)
    SetVehicleMods(vehicle, vehicleData.mods)
    SetVehicleFuelLevel(vehicle, vehicleData.fuel + 0.0)
    SetVehicleEngineHealth(vehicle, vehicleData.engine + 0.0)
    SetVehicleBodyHealth(vehicle, vehicleData.body + 0.0)
    
    -- Set player in vehicle
    TaskWarpPedIntoVehicle(PlayerPedId(), vehicle, -1)
    
    -- Give keys
    TriggerEvent('qb-vehiclekeys:client:SetOwner', vehicleData.plate)
    
    SetModelAsNoLongerNeeded(model)
end)

Exports

Garage Access

-- Check if player owns garage
local hasAccess = exports['qb-garages']:HasGarageAccess(garageId, citizenid)
 
-- Get player's vehicles in specific garage
local vehicles = exports['qb-garages']:GetGarageVehicles(garageId, citizenid)
 
-- Check if garage has available parking spots
local hasSpace = exports['qb-garages']:HasParkingSpace(garageId)

Vehicle Information

-- Get vehicle current status
local status = exports['qb-garages']:GetVehicleStatus(plate)
 
-- Check if vehicle can be spawned
local canSpawn = exports['qb-garages']:CanSpawnVehicle(plate, garageId)
 
-- Calculate repair costs
local repairCost = exports['qb-garages']:CalculateRepairCost(vehicleData)

🎮 Player Experience

Garage Features

  1. Vehicle Storage

    • Secure parking with persistent damage
    • Vehicle condition monitoring
    • Fuel level preservation
    • Modification persistence
  2. Access Control

    • Job-restricted garages
    • Gang territory garages
    • Private garage ownership
    • Shared access permissions
  3. Vehicle Management

    • Real-time vehicle status
    • Location tracking
    • Damage assessment
    • Insurance claims

UI/UX Features

// Garage interface JavaScript
function openGarageMenu(vehicles, garage) {
    $('.garage-container').fadeIn(300);
    $('.garage-title').text(garage.label);
    
    // Populate vehicle list
    vehicles.forEach(vehicle => {
        const vehicleCard = createVehicleCard(vehicle);
        $('.vehicle-list').append(vehicleCard);
    });
}
 
function createVehicleCard(vehicle) {
    const damageClass = getDamageClass(vehicle.engine, vehicle.body);
    
    return `
        <div class="vehicle-card" data-plate="${vehicle.plate}">
            <div class="vehicle-info">
                <h3>${vehicle.model}</h3>
                <p class="plate">${vehicle.plate}</p>
                <div class="status-indicators">
                    <span class="fuel">⛽ ${vehicle.fuel}%</span>
                    <span class="engine ${damageClass}">🔧 ${vehicle.engine}%</span>
                    <span class="body ${damageClass}">🚗 ${vehicle.body}%</span>
                </div>
            </div>
            <div class="vehicle-actions">
                <button class="spawn-btn" ${vehicle.engine < 25 ? 'disabled' : ''}>
                    ${vehicle.engine < 25 ? 'Needs Repair' : 'Take Out'}
                </button>
            </div>
        </div>
    `;
}

🛠️ Advanced Features

Parking Meter System

-- Parking meter implementation
local parkingMeters = {}
 
CreateThread(function()
    while true do
        Wait(3600000) -- Check every hour
        
        for plate, meterData in pairs(parkingMeters) do
            if meterData.expires < os.time() then
                -- Issue parking ticket
                IssueParkingTicket(plate, Config.ParkingCost)
                parkingMeters[plate] = nil
            end
        end
    end
end)
 
function PayParkingMeter(plate, hours)
    local cost = Config.ParkingCost * hours
    local Player = QBCore.Functions.GetPlayerByPlate(plate)
    
    if Player and Player.Functions.RemoveMoney('cash', cost) then
        parkingMeters[plate] = {
            expires = os.time() + (hours * 3600),
            paid = cost
        }
        return true
    end
    
    return false
end

Vehicle Sharing System

-- Share vehicle access with other players
RegisterNetEvent('qb-garages:server:shareVehicle', function(plate, targetCitizenId, duration)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    
    if not Player then return end
    
    -- Verify ownership
    local result = MySQL.Sync.fetchSingle(
        'SELECT * FROM player_vehicles WHERE plate = ? AND citizenid = ?',
        {plate, Player.PlayerData.citizenid}
    )
    
    if result then
        -- Create temporary access
        MySQL.insert('INSERT INTO vehicle_shares (plate, owner_citizenid, shared_citizenid, expires) VALUES (?, ?, ?, ?)', {
            plate,
            Player.PlayerData.citizenid,
            targetCitizenId,
            os.time() + (duration * 3600) -- duration in hours
        })
        
        TriggerClientEvent('QBCore:Notify', src, 'Vehicle access shared successfully', 'success')
    end
end)
 
-- Check shared vehicle access
function HasSharedAccess(plate, citizenid)
    local result = MySQL.Sync.fetchSingle(
        'SELECT * FROM vehicle_shares WHERE plate = ? AND shared_citizenid = ? AND expires > ?',
        {plate, citizenid, os.time()}
    )
    
    return result ~= nil
end

Anti-Theft System

-- Vehicle security measures
local function CreateVehicleSecurity(vehicle, plate)
    local securityLevel = GetVehicleSecurityLevel(vehicle)
    
    if securityLevel > 0 then
        -- Enable alarm system
        SetVehicleAlarm(vehicle, true)
        SetVehicleAlarmTimeLeft(vehicle, 30000) -- 30 seconds
        
        -- Set door locks
        SetVehicleDoorsLocked(vehicle, 2) -- Locked
        
        -- GPS tracking
        if securityLevel >= 2 then
            CreateVehicleGPSTracker(vehicle, plate)
        end
        
        -- Remote engine disable
        if securityLevel >= 3 then
            SetVehicleEngineOn(vehicle, false, true, true)
            SetVehicleUndriveable(vehicle, true)
        end
    end
end
 
-- GPS tracking system
local vehicleTrackers = {}
 
function CreateVehicleGPSTracker(vehicle, plate)
    vehicleTrackers[plate] = {
        vehicle = vehicle,
        lastPosition = GetEntityCoords(vehicle),
        lastUpdate = GetGameTimer()
    }
end
 
CreateThread(function()
    while true do
        Wait(30000) -- Update every 30 seconds
        
        for plate, tracker in pairs(vehicleTrackers) do
            if DoesEntityExist(tracker.vehicle) then
                tracker.lastPosition = GetEntityCoords(tracker.vehicle)
                tracker.lastUpdate = GetGameTimer()
                
                -- Update database
                MySQL.update('UPDATE player_vehicles SET last_position = ? WHERE plate = ?', {
                    json.encode({
                        x = tracker.lastPosition.x,
                        y = tracker.lastPosition.y,
                        z = tracker.lastPosition.z
                    }),
                    plate
                })
            end
        end
    end
end)

🏗️ Integration Examples

With QB-Fuel

-- Fuel integration for garage storage
RegisterNetEvent('qb-garages:server:storeVehicle', function(plate, garage, vehProps)
    -- Get current fuel level from QB-Fuel
    local fuelLevel = exports['qb-fuel']:GetFuel(vehProps.entity)
    
    -- Update vehicle with fuel data
    vehProps.fuel = fuelLevel
    
    -- Continue with normal storage process
    StoreVehicleInGarage(plate, garage, vehProps)
end)

With QB-Inventory (Vehicle Trunk)

-- Trunk integration
RegisterNetEvent('qb-garages:server:openTrunk', function(plate)
    local src = source
    
    -- Check vehicle ownership
    if HasVehicleAccess(src, plate) then
        exports['qb-inventory']:OpenInventory(src, 'trunk', plate, {
            maxweight = GetVehicleTrunkSpace(plate),
            slots = 50,
        })
    end
end)
 
-- Transfer trunk items when storing vehicle
function TransferTrunkToGarage(plate, garage)
    local trunkItems = exports['qb-inventory']:GetInventory('trunk', plate)
    
    if trunkItems and #trunkItems > 0 then
        -- Create temporary storage for trunk items
        local storageId = 'garage_trunk_' .. plate
        exports['qb-inventory']:CreateStorage(storageId, {
            maxweight = 100000,
            slots = 50,
            items = trunkItems
        })
        
        -- Clear trunk
        exports['qb-inventory']:ClearInventory('trunk', plate)
        
        return storageId
    end
    
    return nil
end

With QB-Mechanics

-- Vehicle repair integration
RegisterNetEvent('qb-garages:server:repairVehicle', function(plate, repairType)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    
    if not Player then return end
    
    local repairCost = CalculateRepairCost(plate, repairType)
    
    if Player.Functions.RemoveMoney('bank', repairCost) then
        -- Full repair
        if repairType == 'full' then
            MySQL.update('UPDATE player_vehicles SET engine = 1000, body = 1000 WHERE plate = ?', {plate})
        -- Engine only
        elseif repairType == 'engine' then
            MySQL.update('UPDATE player_vehicles SET engine = 1000 WHERE plate = ?', {plate})
        -- Body only
        elseif repairType == 'body' then
            MySQL.update('UPDATE player_vehicles SET body = 1000 WHERE plate = ?', {plate})
        end
        
        TriggerClientEvent('QBCore:Notify', src, 'Vehicle repaired successfully', 'success')
    else
        TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds for repair', 'error')
    end
end)

💰 Economic Features

Garage Rental System

-- Private garage rental
Config.PrivateGarages = {
    ["garage_1"] = {
        label = "Downtown Private Garage",
        coords = vector3(240.0, -800.0, 30.0),
        monthlyRent = 5000,
        maxVehicles = 10,
        security = "high"
    },
    ["garage_2"] = {
        label = "Sandy Shores Storage",
        coords = vector3(1700.0, 3600.0, 35.0),
        monthlyRent = 2500,
        maxVehicles = 6,
        security = "medium"
    }
}
 
-- Rental payment system
CreateThread(function()
    while true do
        Wait(2592000000) -- Run once per month (30 days)
        
        local rentalDue = MySQL.Sync.fetchAll('SELECT * FROM garage_ownership')
        
        for _, rental in pairs(rentalDue) do
            local garage = Config.PrivateGarages[rental.garage_id]
            if garage then
                local Player = QBCore.Functions.GetPlayerByCitizenId(rental.citizenid)
                
                if Player then
                    if Player.Functions.RemoveMoney('bank', garage.monthlyRent) then
                        TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, 
                            'Garage rental fee paid: $' .. garage.monthlyRent, 'success')
                    else
                        -- Eviction process
                        EvictFromGarage(rental.citizenid, rental.garage_id)
                    end
                end
            end
        end
    end
end)

Vehicle Insurance

-- Insurance claim system
RegisterNetEvent('qb-garages:server:claimInsurance', function(plate)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    
    if not Player then return end
    
    local vehicle = MySQL.Sync.fetchSingle(
        'SELECT * FROM player_vehicles WHERE plate = ? AND citizenid = ?',
        {plate, Player.PlayerData.citizenid}
    )
    
    if vehicle and vehicle.state == 3 then -- Destroyed state
        local insuranceCost = CalculateInsuranceCost(vehicle.vehicle)
        
        if Player.Functions.RemoveMoney('bank', insuranceCost) then
            -- Restore vehicle to garage
            MySQL.update('UPDATE player_vehicles SET state = 1, engine = 1000, body = 1000, garage = ? WHERE plate = ?', {
                'insurance_depot', plate
            })
            
            TriggerClientEvent('QBCore:Notify', src, 'Insurance claim processed', 'success')
        else
            TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds for insurance claim', 'error')
        end
    end
end)

🛡️ Security & Anti-Abuse

Access Validation

-- Comprehensive access checking
function ValidateGarageAccess(src, garageId, action)
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return false end
    
    local garage = Config.Garages[garageId]
    if not garage then return false end
    
    -- Check if garage is open
    if not garage.isOpened then
        return false, "Garage is currently closed"
    end
    
    -- Check job requirement
    if garage.job and Player.PlayerData.job.name ~= garage.job then
        return false, "Job access required"
    end
    
    -- Check gang requirement
    if garage.gang and Player.PlayerData.gang.name ~= garage.gang then
        return false, "Gang access required"
    end
    
    -- Check distance
    local playerCoords = GetEntityCoords(GetPlayerPed(src))
    local distance = #(playerCoords - garage.coords)
    if distance > 10.0 then
        return false, "Too far from garage"
    end
    
    return true
end

Anti-Duplication Protection

-- Prevent vehicle duplication
local spawnCooldowns = {}
 
RegisterNetEvent('qb-garages:server:spawnvehicle', function(plate, garage, coords)
    local src = source
    local citizenid = QBCore.Functions.GetIdentifier(src, 'citizenid')
    
    -- Check spawn cooldown
    local cooldownKey = citizenid .. '_' .. plate
    if spawnCooldowns[cooldownKey] and spawnCooldowns[cooldownKey] > GetGameTimer() then
        TriggerClientEvent('QBCore:Notify', src, 'Please wait before spawning another vehicle', 'error')
        return
    end
    
    -- Check if vehicle already exists in world
    if IsVehicleSpawned(plate) then
        TriggerClientEvent('QBCore:Notify', src, 'Vehicle is already spawned', 'error')
        return
    end
    
    -- Set cooldown (10 seconds)
    spawnCooldowns[cooldownKey] = GetGameTimer() + 10000
    
    -- Continue with spawn process
    SpawnVehicleFromGarage(src, plate, garage, coords)
end)

❓ Troubleshooting

Common Issues

Issue: Vehicles not appearing in garage

-- Debug vehicle states
RegisterCommand('checkgarage', function(source, args)
    local garageId = args[1]
    local vehicles = MySQL.Sync.fetchAll('SELECT * FROM player_vehicles WHERE garage = ?', {garageId})
    
    print('Vehicles in garage ' .. garageId .. ':')
    for _, vehicle in pairs(vehicles) do
        print(string.format('Plate: %s, Model: %s, State: %d', 
            vehicle.plate, vehicle.vehicle, vehicle.state))
    end
end, true)

Issue: Vehicle spawning in wrong location

-- Validate spawn coordinates
local function ValidateSpawnCoords(coords, garageId)
    -- Check if coordinates are on ground
    local ground, z = GetGroundZFor_3dCoord(coords.x, coords.y, coords.z)
    if not ground then
        print('WARNING: Invalid spawn coords for garage ' .. garageId)
        return false
    end
    
    -- Check for obstacles
    local vehicle = GetClosestVehicle(coords.x, coords.y, coords.z, 3.0, 0, 70)
    if vehicle ~= 0 then
        print('WARNING: Spawn location blocked for garage ' .. garageId)
        return false
    end
    
    return true
end

Issue: Database connection problems

-- Test database connectivity
RegisterCommand('testdb', function()
    MySQL.ready(function()
        local result = MySQL.Sync.fetchSingle('SELECT COUNT(*) as count FROM player_vehicles')
        if result then
            print('Database connected. Total vehicles: ' .. result.count)
        else
            print('Database connection failed')
        end
    end)
end, true)

Performance Optimization

-- Optimize garage loading
local garageCache = {}
 
function GetGarageVehicles(garageId, citizenid)
    local cacheKey = garageId .. '_' .. citizenid
    
    if garageCache[cacheKey] and garageCache[cacheKey].expires > GetGameTimer() then
        return garageCache[cacheKey].data
    end
    
    local vehicles = MySQL.Sync.fetchAll(
        'SELECT * FROM player_vehicles WHERE garage = ? AND citizenid = ? AND state = 1',
        {garageId, citizenid}
    )
    
    -- Cache for 30 seconds
    garageCache[cacheKey] = {
        data = vehicles,
        expires = GetGameTimer() + 30000
    }
    
    return vehicles
end

📚 Additional Resources