FiveM & QBCore · Practical guide

QBCore Phone Script — Design, Build, and Secure a Modern In-Game Phone

A practical guide for building a QBCore phone script for FiveM. Covers architecture, server authority, database patterns, examples (fxmanifest, client/server), validation, installation,.

Stellar AI · Updated 8 September 2026 · 5 min read

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

  1. Define server-owned data: phone number, contacts, messages, bill records.
  2. Create resource scaffold: fxmanifest.lua, server.lua, client.lua, UI files (not included here).
  3. Add RPCs and server callbacks for every changeable action; validate on server.
  4. Store persistent data in your database with fail-safe patterns.
  5. 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

ItemPass/FailNotes
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.

Build your next system with Stellar AI

Describe one feature, get organized project files, then bring back your errors to keep improving. Start free with no card required. Test generated code in a private development environment before release.

Create your first script free →