Create a production‑ready in‑game smartphone for FiveM using NUI + React. You will scaffold a resource, wire QBCore/ESX events, persist data in MySQL, and ship a smooth UI that…
Share
Build a Custom Phone App (NUI + React) for QBCore/ESX
Introduction
Goal
Create a production‑ready in‑game smartphone for FiveM using NUI + React. You will scaffold a resource, wire QBCore/ESX events, persist data in MySQL, and ship a smooth UI that respects performance budgets.
This guide is part of our , covering everything from MLO design to scripting, vehicle modding, and building your creator brand.
Prerequisites
with txAdmin and MySQL ().
Node.js 18+ and pnpm or npm on your dev PC.
One framework installed: QBCore or ESX.
Recommended libs: ox_lib (callbacks, notifications), ox_inventory (optional for phone item), ox_target (optional for world interactions).
If I'm using ESX, how does this phone app integrate with usable items?
The guide will walk you through leveraging ESX's `RegisterUsableItem` function. This essential ESX function allows players to trigger the phone's NUI interface by using a designated item, creating a realistic and immersive interaction within the game. The phone item can also be integrated with `ox_inventory`, allowing for a more advanced inventory system.
What's the best way to handle potential conflicts between the phone's NUI and other UI elements, like the pause menu?
The guide addresses the common issue of the phone's NUI opening behind the pause menu. The solution involves disabling the phone's NUI during pause menu activity and re-enabling it once the player returns to the active game state. This process can be achieved by checking if pause is being used and then re-opening the phone NUI.
What if the NUI callback isn't firing correctly when I interact with the phone's interface?
If the NUI callback isn't firing, the guide suggests examining your JavaScript console to ensure that callbacks are firing between the client and server with the correct data. It's essential to check both client-side JavaScript code and server-side Lua code to ensure they are correctly structured and that the event names match. Double-check your `SendNUIMessage` calls and the corresponding event listeners.
Why does this guide recommend Node.js 18+ specifically?
Turn framework research into a launch-ready script stack
Use this guide to narrow the framework decision, then move into the core commercial hubs for verified scripts, curated bundles, and a faster server launch path.
Framework hub
Move into the QBCore landing page to compare verified scripts, framework fit, and install-ready products built for modern FiveM servers.
Open QBCore hub
Framework hub
Use the ESX landing page to compare framework-specific resources, launch guidance, and premium products that fit ESX-first servers.
Open ESX hub
Premium catalog
Move from research into the main shop to compare real products, framework labels, screenshots, and production-ready quality signals.
Open premium shop
Premium Scripts You Might Like
Free Scripts You Might Like
Related Articles
1. Freeze writes during migration (stop the game server + any external bots touching DB). 2. Full backup and a dump of table structures. Store both with timestamps. 3.
Ready to turn your FiveM or Discord community into a profit‑making hub? Our step‑by‑step guide walks you through setting up a Tebex store, choosing a stunning design, adding game‑boosting perks, and m
Want to host your own GTA V multiplayer world with alt:V? This guide shows you two reliable setup paths (Windows & Linux), gives you a clean server.toml, a first working…
Resourcemy_phone with fxmanifest.lua, client, server, and ui bundle.
UI: React app built with Vite into /ui/dist. NUI talks to Lua via postMessage + RegisterNUICallback.
Data: MySQL tables for phone_contacts, phone_messages, phone_calls.
Framework glue: QBCore or ESX item usable handler toggles the phone, and server callbacks load/save data.
Event flow
Player presses key or uses the phone item → 2) SetNuiFocus(true, true) and SendNUIMessage({ action = 'open' }) → 3) React shows UI → 4) UI requests data via fetch('https://my_phone/xyz') (NUI) → 5) RegisterNUICallback('xyz', ...) runs on client/server → 6) Server reads/writes DB → 7) Response returns to UI → 8) Close phone and release focus.
-- client/main.lua
local open = false
local function openPhone()
if open then return end
open = true
SetNuiFocus(true, true)
SendNUIMessage({ action = 'open' })
end
local function closePhone()
if not open then return end
open = false
SetNuiFocus(false, false)
SendNUIMessage({ action = 'close' })
end
-- Keybind (F1 example)
RegisterCommand('myphone', function()
if open then closePhone() else openPhone() end
end)
-- NUI → game callbacks
RegisterNUICallback('ui:close', function(_, cb)
closePhone()
cb({ ok = true })
end)
-- list contacts asks the server
RegisterNUICallback('contacts:list', function(_, cb)
lib.callback('my_phone:server:getContacts', false, function(rows)
cb(rows)
end)
end)
Tip: enable NUI devtools in game console with nui_devTools. Open http://localhost:13172 in your Chromium browser to inspect the UI.
Step 4 — Server: DB schema + callbacks
SQL (MySQL)
CREATE TABLE IF NOT EXISTS phone_contacts (
id INT AUTO_INCREMENT PRIMARY KEY,
citizenid VARCHAR(64) NOT NULL,
name VARCHAR(64) NOT NULL,
number VARCHAR(32) NOT NULL,
INDEX(citizenid)
);
CREATE TABLE IF NOT EXISTS phone_messages (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
owner VARCHAR(64) NOT NULL,
peer VARCHAR(64) NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX(owner), INDEX(peer)
);
Server with oxmysql + ox_lib
-- server/main.lua
local QBCore = exports['qb-core'] and exports['qb-core']:GetCoreObject()
ESX = ESX or nil
if not QBCore then
TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
end
-- Load contacts for the logged-in character
lib.callback.register('my_phone:server:getContacts', function(source)
local citizenid
if QBCore then
local Player = QBCore.Functions.GetPlayer(source)
citizenid = Player and Player.PlayerData.citizenid
else
local xPlayer = ESX.GetPlayerFromId(source)
citizenid = xPlayer and xPlayer.identifier
end
if not citizenid then return {} end
local rows = MySQL.query.await('SELECT id, name, number FROM phone_contacts WHERE citizenid = ?', { citizenid })
return rows or {}
end)
-- Save a contact
lib.callback.register('my_phone:server:addContact', function(source, contact)
if type(contact) ~= 'table' then return { ok = false } end
local name, number = contact.name, contact.number
if not name or not number then return { ok = false } end
local citizenid
if QBCore then
local Player = QBCore.Functions.GetPlayer(source)
citizenid = Player and Player.PlayerData.citizenid
else
local xPlayer = ESX.GetPlayerFromId(source)
citizenid = xPlayer and xPlayer.identifier
end
if not citizenid then return { ok = false } end
MySQL.insert.await('INSERT INTO phone_contacts (citizenid, name, number) VALUES (?, ?, ?)', { citizenid, name, number })
return { ok = true }
end)
Add a usable phone item and toggle the UI when used.
-- server/main.lua (QBCore only)
if QBCore then
QBCore.Functions.CreateUseableItem('phone', function(src, item)
TriggerClientEvent('my_phone:client:toggle', src)
end)
end
-- client/main.lua
RegisterNetEvent('my_phone:client:toggle', function()
if IsPauseMenuActive() then return end
if IsPedInAnyVehicle(PlayerPedId(), false) then -- optional rule
-- show a notification via ox_lib
lib.notify({ title = 'Phone', description = 'No phone while driving.', type = 'error' })
return
end
if IsNuiFocused() then ExecuteCommand('myphone') else ExecuteCommand('myphone') end
end)
ESX
-- server/main.lua (ESX only)
if ESX and not QBCore then
ESX.RegisterUsableItem('phone', function(playerId)
TriggerClientEvent('my_phone:client:toggle', playerId)
end)
end
If you use ox_inventory, create the item there and rely on its usable handlers. You can still trigger the same client event.
Step 6 — Core features
Implement small slices and ship incrementally.
Contacts
UI calls contacts:list → server returns rows.
Add “Add contact” form → call addContact.
Add “Remove contact” → server deletes by id with citizen ownership check.
Messages (SMS)
Table phone_messages stores owner, peer, body.
UI opens a chat, calls messages:list and messages:send.
Server inserts message, optionally emits client event to peer if online.
Server sketch
lib.callback.register('my_phone:server:messages:list', function(source, peer)
local cid = GetCitizenId(source)
return MySQL.query.await('SELECT * FROM phone_messages WHERE owner=? AND peer=? ORDER BY id DESC LIMIT 200', { cid, peer }) or {}
end)
RegisterNetEvent('my_phone:server:messages:send', function(peer, body)
local src = source
local cid = GetCitizenId(src)
if type(body) ~= 'string' or #body == 0 or #body > 500 then return end
MySQL.insert.await('INSERT INTO phone_messages (owner, peer, body) VALUES (?, ?, ?)', { cid, peer, body })
TriggerClientEvent('my_phone:client:messages:push', src, peer, body)
-- optional: find target player by phone number and push live event
end)
Client receive
RegisterNetEvent('my_phone:client:messages:push', function(peer, body)
SendNUIMessage({ action = 'message:new', peer = peer, body = body })
end)
Calls (optional MVP)
Store call logs only. Real audio uses your voice plugin (pma-voice, mumble, SaltyChat) and is outside this MVP.
Add UI keypad → on dial, log an outgoing call; on answer, log incoming. You can integrate later with a voice plugin’s API.
Step 7 — Security, UX, performance
Security
Never trust NUI input. Validate types and length on server.
Check ownership on every query with citizenid or identifier.
Avoid exposing identifiers to other clients. Use server relays.
UX
Cancel phone while downed, cuffed, or driving, if your server rules require it.
Keep UI snappy. Use optimistic updates and reconcile on server ack.
Performance
Keep NUI idle. Avoid setInterval loops in React. Use effects and events.
Keep bundles small. Lazy‑load heavy screens. Ship compressed assets.
Use Resmon to budget under 0.01–0.02 ms on average. See FiveMX guide linked above.
Step 8 — Testing & debugging
Start resource in server.cfg before dependent scripts.
ensure my_phone
In game, press F8 → run nui_devTools → open http://localhost:13172 and pick your NUI page.
Run Resmon and verify CPU stays low while the phone is open and closed.
Step 9 — Packaging & updates
Commit ui/ source and ui/dist/ build.
In CI, run pnpm --filter ui build and ship only dist in releases.
Version your SQL migrations. Never drop user data without backups.
Step 10 — Extensions you can add next
Banking: link to your server’s banking resource; expose balance and transfers.
Tweets/Ads: global feed with rate limits and moderation.
Marketplace: listings with escrow.
Job apps: police/EMS MDT hooks.
Photos: screenshot integration via server endpoint, not data URLs.
Settings: dynamic themes, ringtones, backgrounds.
Troubleshooting
Phone opens behind pause menu
Phone opens behind pause menu
Disable during pause checks and reopen when active returns.
NUI callback not firing
Ensure RegisterNUICallback('event', ...) names match the UI fetch path.
Confirm fx_version is cerulean and ui_page points to ui/dist/index.html.
Check F8 console for CORS or JSON errors.
Items not usable
QBCore: confirm QBCore.Functions.CreateUseableItem runs and phone exists in your item list.
ESX: confirm ESX.RegisterUsableItem('phone', ...) registers after inventory loads.
Database errors
Ensure oxmysql started before this resource.
Check column sizes and encodings for Unicode names.
Reference snippets (copy‑paste)
Helper
function GetCitizenId(source)
if QBCore then
local P = QBCore.Functions.GetPlayer(source)
return P and P.PlayerData.citizenid
else
local xP = ESX.GetPlayerFromId(source)
return xP and xP.identifier
end
end
Add contact from UI
// UI
async function addContact(name: string, number: string) {
const res = await nui<{ ok: boolean }>('contacts:add', { name, number })
if (res.ok) {
const next = await nui<any[]>('contacts:list')
// update state
}
}
Node.js 18+ is recommended to leverage modern JavaScript features and ensure compatibility with the latest versions of React and other related libraries, which means this will ensure that there are no issues while creating the phone app with the most up-to-date tooling. Staying current with Node.js versions will provide access to the latest performance improvements and security patches.
What is Build a Custom Phone App (NUI + React) for QBCore/ESX?
Goal Create a production‑ready in‑game smartphone for FiveM using NUI + React. You will scaffold a resource, wire QBCore/ESX events, persist data in MySQL, and ship a smooth UI that respects performance budgets.