Skip to main content
TRACK B·BUILD REAL PRODUCTS·Verified June 2026 · Lua 5.4 · ox_lib 3.x
Learning with an AI assistant?
Copies this lesson plus 2026 ground rules (no lua54 'yes', Cfx.re Portal, correct callback signatures) as a ready-to-paste mentor prompt.

Vehicles & garages

A garage is a valet with a clipboard. The clipboard is your database. The plate is the ticket. Owning a vehicle in FiveM is not the spawned car on the street, it is a row in a table: a unique plate, a model, and a state column that records whether the car is out right now or sitting in the garage. This lesson builds the full take and store flow, then takes it apart so you understand why the plate is the key and why the state column, not the spawning, is the actual garage.

You'll learn
Plate as primary key, the garage state column, the take/store lifecycle, CreateVehicleServerSetter, parameterized upserts
Time
~24 minutes
Prereqs
P1 (oxmysql + a MariaDB database), P7 (OneSync enabled)
Outcome
A working garage where /takecar spawns and persists a car by plate, and /storecar flips its state back and despawns it
BEFORE YOU START

Build it

Make the resource folder

The server has one folder for this lesson.

Inside your server's resources folder, create this folder:

code
resources/qu_vehicles_garages

Create the files

Every file named in the manifest exists.

Create this exact file layout:

code
resources/qu_vehicles_garages/
fxmanifest.lua
server.lua

Write fxmanifest.lua

FiveM loads oxmysql first, then your server file.

Open fxmanifest.lua and paste this:

code
fx_version 'cerulean'
game 'gta5'

server_script '@oxmysql/lib/MySQL.lua'
server_script 'server.lua'

dependencies {
'oxmysql',
'/onesync'
}

oxmysql is a community library, not a built-in Cfx.re resource, so it must already be installed and started. The dependency list also uses Cfx.re's documented /onesync runtime constraint. FiveM refuses to start this lesson unless both requirements are available, which is clearer than letting the first query or server-side spawn fail later.

Create the table

The garage clipboard exists in your database.

Run this SQL against your MariaDB database once, before you start the resource:

code
CREATE TABLE IF NOT EXISTS qu_owned_vehicles (
plate VARCHAR(12) NOT NULL,
model VARCHAR(40) NOT NULL,
state VARCHAR(20) NOT NULL DEFAULT 'garage',
PRIMARY KEY (plate)
);

The PRIMARY KEY (plate) line is the part that makes the rest of the lesson work. It tells the database that no two rows may share a plate. That single rule is what lets one plate stand for one car forever.

Write the lesson code

The take/store lifecycle is now runnable code.

Open server.lua and paste this:

code
local PLATE = 'QUCAR1'
local MODEL_NAME = 'sultan'
local MODEL_HASH = joaat(MODEL_NAME)

local spawnedVehicle = nil

RegisterCommand('takecar', function()
if spawnedVehicle and DoesEntityExist(spawnedVehicle) then
    print('[qu_vehicles_garages] refused: QUCAR1 is already out')
    return
end

local vehicle = CreateVehicleServerSetter(MODEL_HASH, 'automobile', 215.0, -810.0, 30.0, 90.0)
if vehicle == 0 then
    print('[qu_vehicles_garages] spawn failed')
    return
end

SetEntityOrphanMode(vehicle, 2)
SetVehicleNumberPlateText(vehicle, PLATE)
spawnedVehicle = vehicle

local rows = MySQL.update.await(
    'INSERT INTO qu_owned_vehicles (plate, model, state) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE state = ?',
    { PLATE, MODEL_NAME, 'out', 'out' }
)

print('[qu_vehicles_garages] took ' .. PLATE .. ' rows affected ' .. rows)
end, true)

RegisterCommand('storecar', function()
if spawnedVehicle and DoesEntityExist(spawnedVehicle) then
    DeleteEntity(spawnedVehicle)
    spawnedVehicle = nil
end

local affected = MySQL.update.await(
    'UPDATE qu_owned_vehicles SET state = ? WHERE plate = ?',
    { 'garage', PLATE }
)

print('[qu_vehicles_garages] stored ' .. PLATE .. ' rows updated ' .. affected)
end, true)

Start and test it

The expected proof appears in the server console.

Open server.cfg and add this line:

code
ensure qu_vehicles_garages

Save, then run:

code
restart qu_vehicles_garages

Run this test in the txAdmin Live Console, twice in a row:

code
takecar then storecar

The first time you run takecar the row is created, so one row is affected. storecar then flips one row back to garage. The second time you run takecar the row already exists, so the upsert updates it and reports two rows affected (MariaDB counts an updated upsert row as two).

Keep reading the full lesson

Sign in to start, then unlock every step of this lesson and the full FiveM School with a membership.

Still ahead in this lesson
  • How it works
  • If something went wrong
  • What you can do now
  • Try it yourself

The remainder of Vehicles and garages is available to FiveM School members.