This guide walks you from planning to a minimal, secure QBCore phone resource. It focuses on server authority, validation, data storage, and integration points so you ship a dependable phone feature on your FiveM server.
Overview and goals
An in-game phone in QBCore is a small app platform: contacts, messages, calls, job notices, and UI-ready events. The highest-risk areas are client trust, persistent data, and third-party integrations (marketplace items, banking). This guide shows a minimal, secure implementation pattern and a clear plan for expanding features safely.
Short implementation plan
- Define server-owned data: phone number, contacts, messages, bill records.
- Create resource scaffold: fxmanifest.lua, server.lua, client.lua, UI files (not included here).
- Add RPCs and server callbacks for every changeable action; validate on server.
- Store persistent data in your database with fail-safe patterns.
- Hook UI events to safe client triggers and server callbacks.
Assumptions and dependencies
This guide assumes QBCore v2-style exports and callbacks (QBCore.Functions.TriggerCallback / CreateCallback). If your server uses a different QBCore fork or ox_lib helpers, adapt export names accordingly. You need a MySQL driver (oxmysql or ghmattimysql) already configured by your server. If you prefer scaffolding or AI-assisted file generation, try the Stellar AI app at https://trystellarai.com/app to create destination-labeled files.
Core file examples (destination-labelled)
Below are complete minimal files for a resource named qb-simplephone. Place them inside resources/qb-simplephone/ and start with ensure qb-simplephone in your server.cfg.
-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
author 'YourName'
description 'Minimal QBCore phone scaffold'
version '1.0.0'
shared_script '@qb-core/shared/locale.lua'
server_script 'server.lua'
client_script 'client.lua'
ui_page 'html/index.html'
files {
'html/index.html', 'html/app.js', 'html/app.css'
}
-- server.lua (complete minimal server authority patterns)
local QBCore = exports['qb-core']:GetCoreObject()
-- Example persistent storage table: messages
QBCore.Functions.CreateCallback('qb-simplephone:GetMessages', function(source, cb)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then
return cb({})
end
local citizenid = Player.PlayerData.citizenid
-- Use your DB driver here. Pseudocode using oxmysql
MySQL.Async.fetchAll('SELECT * FROM phone_messages WHERE citizenid = @cid', {['@cid'] = citizenid}, function(result)
cb(result or {})
end)
end)
RegisterNetEvent('qb-simplephone:SendMessage')
AddEventHandler('qb-simplephone:SendMessage', function(targetNumber, messageText)
local src = source
if type(targetNumber) ~= 'string' or type(messageText) ~= 'string' then return end
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local senderCid = Player.PlayerData.citizenid
-- Validate message length and content server-side
if #messageText > 500 then
-- optionally log and reject
return
end
-- Look up target's citizenid and insert message
local target = MySQL.Sync.fetchAll('SELECT citizenid FROM players WHERE phone_number = @num', {['@num'] = targetNumber})
if target[1] then
local targetCid = target[1].citizenid
MySQL.Async.execute('INSERT INTO phone_messages (citizenid, sender, message, time) VALUES (@cid, @sender, @msg, @t)', {
['@cid'] = targetCid, ['@sender'] = senderCid, ['@msg'] = messageText, ['@t'] = os.time()
})
-- Optionally notify target player if online
local targetPlayer = QBCore.Functions.GetPlayerByCitizenId(targetCid)
if targetPlayer then
TriggerClientEvent('qb-simplephone:NewMessage', targetPlayer.PlayerData.source, {from = senderCid, text = messageText})
end
end
end)
Client code should never write to the DB, and only send intent events to the server. Validate in both layers to protect UX and security.
Client pattern (short)
-- client.lua (only UI triggers and listeners)
local QBCore = exports['qb-core']:GetCoreObject()
RegisterNetEvent('qb-simplephone:NewMessage')
AddEventHandler('qb-simplephone:NewMessage', function(msg)
-- Forward to UI: use SendNUIMessage or qb-phone compatible event
SendNUIMessage({action = 'new_message', payload = msg})
end)
-- Example: request messages when opening phone
function OpenPhone()
QBCore.Functions.TriggerCallback('qb-simplephone:GetMessages', function(messages)
SendNUIMessage({action = 'load_messages', messages = messages})
end)
SetNuiFocus(true, true)
end
Checklist before you deploy
| Item | Pass/Fail | Notes |
|---|---|---|
| Server-side validation for all client actions | ✔ | Check data types and lengths |
| No client DB writes | ✔ | DB access only from server scripts |
| Phone number uniqueness enforced | ✔ | DB unique index on phone_number |
| Rate limiting for messaging/calls | ✖ | Add throttling if spam observed |
| Backup plan for DB failures | ✔ | Return empty sets and queue writes |
Security and validation notes
- Treat the server as the source of truth: player identifiers, balances, and item ownership must be checked server-side before any transfer or payment.
- Use prepared statements / parameterized queries to avoid injection when using raw SQL drivers.
- Apply throttling for expensive actions (e.g., sending messages) and log suspicious activity for moderation.
- Provide graceful DB failure handling: when reads fail return an empty response and retry or queue writes until DB is available.
Roblox comparison note
If you port the concept to Roblox, the same server-authoritative principle applies. Use RemoteEvents/RemoteFunctions with strict server validation, and persist phone-like data using DataStoreService with exponential backoff and error handling for throttled saves. Keep UI code in LocalScripts and never trust client-sent identifiers for purchases or balances.
Next steps & tooling
After the minimal resource, add features incrementally: contact syncing, in-game VoIP integration, notification routing, and asynchronous push notifications. For generating scaffolds or planning destination-labelled files, consider using an AI scripting workspace to produce file lists and templates; the Stellar AI app can help accelerate iterations: https://trystellarai.com/app. For reading deeper posts on game script patterns, visit the Stellar AI blog: https://trystellarai.com/blog.
Always validate client inputs on the server, treat DB as critical infrastructure with retries and backups, and log edge cases. Start small, test with staff users, then open to the community.
Test contact and message ownership
Use two test accounts and confirm each account can access only its own private contacts and message history. Check invalid recipients, long messages, repeated sends, and reconnecting while an update is pending. Interface controls should explain a rejected action rather than leaving a loading indicator forever. Keep logs free of private message contents unless strictly needed for an authorized diagnostic session. Document which component owns the data before connecting additional phone applications.