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.
Build it
Make the resource folder
Inside your server's resources folder, create this folder:
resources/qu_vehicles_garages
Create the files
Create this exact file layout:
resources/qu_vehicles_garages/
fxmanifest.lua
server.lua
Write fxmanifest.lua
Open fxmanifest.lua and paste this:
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
Run this SQL against your MariaDB database once, before you start the resource:
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
Open server.lua and paste this:
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
Open server.cfg and add this line:
ensure qu_vehicles_garages
Save, then run:
restart qu_vehicles_garages
Run this test in the txAdmin Live Console, twice in a row:
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.
- 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.