Capstone: a mini bank, step by step
By the end of this lesson you will run a server-console /bank command that changes a practice ledger in MariaDB. This is not yet a framework bank: it does not remove pocket cash on deposit or grant pocket cash on withdrawal. It isolates the database rule so you can prove validation before connecting it to real money.
Build it
Make the resource folder
Inside your server's resources folder, create this folder:
resources/qu_bank
Create the files
Create this exact file layout:
resources/qu_bank/
fxmanifest.lua
server.lua
This is a server-only resource. There is no client file and no NUI, so there is no F8 output to read. Every line you see will land in the txAdmin Live Console.
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'
}
Notice there is no lua54 'yes' line. As of June 2025 that directive is deprecated and ignored, since Lua 5.4 is now the only Lua runtime. The dependencies { 'oxmysql' } block is the canonical plural form: it tells the server to start oxmysql before qu_bank, so the @oxmysql/lib/MySQL.lua import resolves. You may see the singular dependency 'oxmysql' in older code; it is tolerated, but the plural block is the form to learn.
Create the balance table
Run this SQL in your database before starting the resource. oxmysql works with both MySQL and MariaDB; MariaDB is the most common choice for FiveM servers and benchmarks fastest in oxmysql's own tests:
CREATE TABLE IF NOT EXISTS qu_bank_accounts (
citizenid VARCHAR(64) NOT NULL,
balance INT NOT NULL DEFAULT 0,
PRIMARY KEY (citizenid)
);
The table keys on a character id, not an auto-increment row id. QBCore and Qbox call that value citizenid; ESX uses its active character identifier instead. The primary key makes one ledger row per active character id.
Write the lesson code
Open server.lua and paste this:
-- For the lesson we use one fixed account so you can test from the console.
-- In a real server this comes from the player's framework identity.
local TEST_CITIZEN = 'ABC12345'
-- Read the balance, creating the account at 0 on first touch.
local function getBalance(citizenid)
local row = MySQL.single.await(
'SELECT balance FROM qu_bank_accounts WHERE citizenid = ?',
{ citizenid }
)
if not row then
MySQL.insert.await(
'INSERT INTO qu_bank_accounts (citizenid, balance) VALUES (?, ?)',
{ citizenid, 0 }
)
return 0
end
return row.balance
end
RegisterCommand('bank', function(source, args)
local action = args[1]
local amount = tonumber(args[2])
local citizenid = TEST_CITIZEN
if action ~= 'balance' and (not amount or amount <= 0 or amount ~= math.floor(amount)) then
print('[qu_bank] amount must be a positive whole number')
return
end
local balance = getBalance(citizenid)
if action == 'balance' then
print('[qu_bank] balance for ' .. citizenid .. ' is ' .. balance)
elseif action == 'deposit' then
local newBalance = balance + amount
MySQL.update.await(
'UPDATE qu_bank_accounts SET balance = ? WHERE citizenid = ?',
{ newBalance, citizenid }
)
print('[qu_bank] deposited ' .. amount .. ', balance now ' .. newBalance)
elseif action == 'withdraw' then
if amount > balance then
print('[qu_bank] REJECTED withdraw of ' .. amount .. ', balance is only ' .. balance)
return
end
local newBalance = balance - amount
MySQL.update.await(
'UPDATE qu_bank_accounts SET balance = ? WHERE citizenid = ?',
{ newBalance, citizenid }
)
print('[qu_bank] withdrew ' .. amount .. ', balance now ' .. newBalance)
else
print('[qu_bank] usage: bank deposit|withdraw|balance <amount>')
end
end, true)
Start and test it
Open server.cfg and add this line:
ensure qu_bank
Save. If this is the first time you are starting the resource this session, type ensure qu_bank in the txAdmin Live Console to start it. After any later edit to the files, use restart qu_bank to reload it:
ensure qu_bank
Now run these four commands in order, from the txAdmin Live Console:
bank balance
bank deposit 100
bank withdraw 250
bank withdraw 40
The third line is the whole point. You asked to withdraw 250 from a balance of 100, and the server said no. The balance never went negative, and the fourth line proves the account still works afterward.
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
- Connecting it to real money (ESX and QBCore)
The remainder of Mini banking capstone is available to FiveM School members.