Multi-character and identity systems
Identity is the spine of a roleplay server: who is this player, which character are they on, and what belongs to that character. Every other script, money, inventory, jobs, keys off the character identifier, so if the identity layer is wrong, everything downstream is wrong. This lesson builds a tiny identity store you can run, then takes it apart so you understand the model instead of trusting a framework blindly.
Build it
This is a server-only resource. There is no client file and no F8 output: identity lives entirely on the server, against the database, which is the whole point. You will run one command in the server console and read the proof there.
Make the resource folder
Inside your server's resources folder, create this folder:
resources/qu_identity
Create the files
Create this exact file layout:
resources/qu_identity/
fxmanifest.lua
server.lua
Write fxmanifest.lua and the tables
Open fxmanifest.lua and paste this:
fx_version 'cerulean'
game 'gta5'
server_script '@oxmysql/lib/MySQL.lua'
server_script 'server.lua'
dependencies {
'oxmysql'
}
Then run this SQL against your MariaDB before starting the resource. These two tables are the entire identity model in miniature:
CREATE TABLE IF NOT EXISTS qu_accounts (
license VARCHAR(64) NOT NULL,
PRIMARY KEY (license)
);
CREATE TABLE IF NOT EXISTS qu_characters (
citizenid VARCHAR(16) NOT NULL,
license VARCHAR(64) NOT NULL,
first_name VARCHAR(40) NOT NULL,
money INT NOT NULL DEFAULT 0,
PRIMARY KEY (citizenid),
UNIQUE KEY uniq_license_name (license, first_name),
FOREIGN KEY (license) REFERENCES qu_accounts (license)
);
Write the lesson code
Open server.lua and paste this:
local function makeCitizenId()
return ('QU%07d'):format(math.random(0, 9999999))
end
local function ensureCharacter(license, firstName)
local existing = MySQL.scalar.await(
'SELECT citizenid FROM qu_characters WHERE license = ? AND first_name = ?',
{ license, firstName }
)
if existing then return existing end
local citizenid = makeCitizenId()
MySQL.insert.await('INSERT INTO qu_characters (citizenid, license, first_name) VALUES (?, ?, ?)',
{ citizenid, license, firstName })
return citizenid
end
RegisterCommand('identitytest', function()
local license = 'license:qu000000000000000000000000000000demo'
MySQL.insert.await('INSERT IGNORE INTO qu_accounts (license) VALUES (?)', { license })
local charA = ensureCharacter(license, 'Avery')
local charB = ensureCharacter(license, 'Blake')
MySQL.update.await('UPDATE qu_characters SET money = ? WHERE citizenid = ?', { 5000, charA })
local moneyA = MySQL.scalar.await('SELECT money FROM qu_characters WHERE citizenid = ?', { charA })
local moneyB = MySQL.scalar.await('SELECT money FROM qu_characters WHERE citizenid = ?', { charB })
print(('[qu_identity] one license: %s'):format(license))
print(('[qu_identity] character A %s money %d'):format(charA, moneyA))
print(('[qu_identity] character B %s money %d'):format(charB, moneyB))
end, true)
Start and test it
Open server.cfg and add this line:
ensure qu_identity
Save, then run this in the server console (txAdmin Live Console):
restart qu_identity
Now run the test command in the same console:
identitytest
The two QU ids are random, so yours will differ. What must match is the shape: both characters hang off the same license line, character A holds 5000, and character B holds 0. You just stored money against a character, and the other character on the same account never saw it.
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 Multi-character and identity systems is available to FiveM School members.