Skip to main content
  • Instant digital delivery
  • Lifetime updates on selected products
  • Trusted by server owners
FiveMX
Shop
Full ServersBundlesNew releases
FiveMX

Start building your server today.

Curated FiveM resources, instant delivery, free starter mods, and practical guides in one calm marketplace.

Browse the shopsupport@fivemx.com

Shop

  • Shop
  • FiveM Mods
  • All Products
  • Free Mods
  • Best Scripts & Mods
  • FiveM Scripts

Frameworks

  • QBCore Scripts
  • ESX Scripts
  • QBox
  • Standalone

Community

  • Blog
  • Support
  • Creators
  • Affiliate

Legal

  • Privacy Policy
  • Terms of Service
  • Refund Policy
  • Digital Delivery
  • Cookie Policy
  • GDPR Compliance
  • DMCA
  • Imprint
  • Editorial Policy

Server Templates

  • QBCore Server Template
  • ESX Server Template
  • NoPixel Server Template
  • Server Packs
  • Free Server Templates
  • Tebex Alternative
© 2026 FiveMX. All rights reserved.·FiveMX is not affiliated with Rockstar Games, Take-Two Interactive, or CFX.re. All trademarks are property of their respective owners.
DiscordDocs
  1. Home
  2. Blog
  3. Development
Table of Contents
IntroductionPrerequisitesArchitectureStep 1 — Scaffold the resourceStep 2 — Create the React NUIStep 3 — Client: open/close, NUI focus, callbacksStep 4 — Server: DB schema + callbacksStep 5 — Framework integration (item + permissions)QBCoreESXStep 6 — Core featuresContactsMessages (SMS)Calls (optional MVP)Step 7 — Security, UX, performanceStep 8 — Testing & debuggingStep 9 — Packaging & updatesStep 10 — Extensions you can add nextTroubleshootingPhone opens behind pause menuReference snippets (copy‑paste)What you builtFurther readingMetaRelated Guides

Build a Custom Phone App (NUI + React) for QBCore/ESX

Published on October 4, 2025·by Lars Miller(Founder & Lead Editor)·Credentials·6 min read·Updated on March 24, 2026

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
Build a Custom Phone App (NUI + React) for QBCore/ESX

Introduction

FiveM Phone Scripts Comparison and Features

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

  1. with txAdmin and MySQL ().
  2. Node.js 18+ and pnpm or npm on your dev PC.
  3. One framework installed: QBCore or ESX.
  4. Recommended libs: ox_lib (callbacks, notifications), ox_inventory (optional for phone item), ox_target (optional for world interactions).
  5. Basic React knowledge.

Docs

  • Cfx.re NUI overview – https://docs.fivem.net/docs/scripting-manual/nui-development/
  • NUI callbacks – https://docs.fivem.net/docs/scripting-manual/nui-development/nui-callbacks/
  • SendNUIMessage – https://docs.fivem.net/docs/scripting-reference/runtimes/lua/functions/SendNUIMessage/
  • Debug NUI devtools – https://docs.fivem.net/docs/scripting-manual/nui-development/full-screen-nui/

Frequently Asked Questions

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?

Table of Contents

IntroductionPrerequisitesArchitectureStep 1 — Scaffold the resourceStep 2 — Create the React NUIStep 3 — Client: open/close, NUI focus, callbacksStep 4 — Server: DB schema + callbacksStep 5 — Framework integration (item + permissions)QBCoreESXStep 6 — Core featuresContactsMessages (SMS)Calls (optional MVP)Step 7 — Security, UX, performanceStep 8 — Testing & debuggingStep 9 — Packaging & updatesStep 10 — Extensions you can add nextTroubleshootingPhone opens behind pause menuReference snippets (copy‑paste)What you builtFurther readingMetaRelated Guides

More on This Topic

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

Disclosure: Some links below are affiliate links to FiveMX products. We may earn a commission at no extra cost to you.

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.

August 16, 2025

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

February 5, 2024

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…

September 22, 2025
  • QBCore server functions – https://docs.qbcore.org/qbcore-documentation/qb-core/server-function-reference
  • ESX RegisterUsableItem – https://docs.esx-framework.org/en/esx_core/es_extended/server/functions
  • ox_lib callbacks – https://overextended.dev/ox_lib
  • Internal reading (FiveMX)

    • Resmon & performance – https://fivemx.com/blog/how-to-use-resmon-in-fivem-optimize-resources/
    • Performance hub – https://fivemx.com/performance
    • Phone scripts market overview – https://fivemx.com/blog/build-a-custom-phone-app

    Architecture

    1. Resource my_phone with fxmanifest.lua, client, server, and ui bundle.
    2. UI: React app built with Vite into /ui/dist. NUI talks to Lua via postMessage + RegisterNUICallback.
    3. Data: MySQL tables for phone_contacts, phone_messages, phone_calls.
    4. Framework glue: QBCore or ESX item usable handler toggles the phone, and server callbacks load/save data.

    Event flow

    1. 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.

    Step 1 — Scaffold the resource

    Folder layout

    resources/ [local]/

        my_phone/
          fxmanifest.lua
          client/
            main.lua
          server/
            main.lua
          ui/
            index.html
            src/
              main.tsx
              App.tsx
              api.ts
              styles.css
    

    fxmanifest.lua

    fx_version 'cerulean'
    game 'gta5'
    
    ui_page 'ui/dist/index.html'
    
    files { 'ui/dist/**' }
    
    client_scripts { 'client/main.lua' }
    server_scripts {
      '@oxmysql/lib/MySQL.lua',
      'server/main.lua'
    }
    
    lua54 'yes'
    

    More info about: fxmanifest.lua


    Step 2 — Create the React NUI

    Initialize a Vite React app inside my_phone/ui and build to ui/dist.

    cd my_phone/ui pnpm create vite@latest . --template react-ts pnpm i

    Vite config (ensure assets land in dist)

    // vite.config.ts
    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    export default defineConfig({
      plugins: [react()],
      build: { outDir: 'dist', emptyOutDir: true },
      base: ''
    })
    

    NUI bridge

    // src/api.ts
    export async function nui<T>(event: string, data?: unknown): Promise<T> {
      const res = await fetch(\`https://my_phone/${event}\`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data ?? {})
      })
      return await res.json()
    }
    

    Mount React + message listener

    // src/main.tsx
    import React from 'react'
    import ReactDOM from 'react-dom/client'
    import App from './App'
    
    const root = ReactDOM.createRoot(document.getElementById('root')!)
    root.render(<App />)
    
    window.addEventListener('message', (e) => {
      if (e.data?.action === 'open') document.body.classList.add('open')
      if (e.data?.action === 'close') document.body.classList.remove('open')
    })
    

    Basic UI

    // src/App.tsx
    import { useEffect, useState } from 'react'
    import { nui } from './api'
    
    type Contact = { id: number; name: string; number: string }
    
    export default function App() {
      const [contacts, setContacts] = useState<Contact[]>([])
      const [visible, setVisible] = useState(false)
    
      useEffect(() => {
        const handler = (e: MessageEvent) => {
          if (e.data?.action === 'open') {
            setVisible(true)
            nui<Contact[]>('contacts:list').then(setContacts)
          }
          if (e.data?.action === 'close') setVisible(false)
        }
        window.addEventListener('message', handler)
        return () => window.removeEventListener('message', handler)
      }, [])
    
      if (!visible) return null
      return (
        <div className="phone">
          <header>Phone</header>
          <section>
            {contacts.map(c => (
              <div key={c.id} className="row">
                <div>{c.name}</div>
                <div>{c.number}</div>
              </div>
            ))}
          </section>
          <footer>
            <button onClick={() => nui('ui:close')}>Close</button>
          </footer>
        </div>
      )
    }
    

    index.html

    <!doctype html>
    <html>
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>my_phone</title>
        <link rel="stylesheet" href="/src/styles.css" />
      </head>
      <body>
        <div id="root"></div>
        <script type="module" src="/src/main.tsx"></script>
      </body>
    </html>
    

    Build the UI:

    pnpm build


    Step 3 — Client: open/close, NUI focus, callbacks

    -- 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)
    

    RegisterKeyMapping('myphone', 'Toggle Phone', 'keyboard', 'F1')

    -- 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)
    

    Step 5 — Framework integration (item + permissions)

    QBCore

    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

    1. UI calls contacts:list → server returns rows.
    2. Add “Add contact” form → call addContact.
    3. Add “Remove contact” → server deletes by id with citizen ownership check.

    Messages (SMS)

    1. Table phone_messages stores owner, peer, body.
    2. UI opens a chat, calls messages:list and messages:send.
    3. 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

    1. Never trust NUI input. Validate types and length on server.
    2. Check ownership on every query with citizenid or identifier.
    3. Avoid exposing identifiers to other clients. Use server relays.

    UX

    1. Cancel phone while downed, cuffed, or driving, if your server rules require it.
    2. Keep UI snappy. Use optimistic updates and reconcile on server ack.

    Performance

    1. Keep NUI idle. Avoid setInterval loops in React. Use effects and events.
    2. Keep bundles small. Lazy‑load heavy screens. Ship compressed assets.
    3. Use Resmon to budget under 0.01–0.02 ms on average. See FiveMX guide linked above.

    Step 8 — Testing & debugging

    1. Start resource in server.cfg before dependent scripts.
    ensure my_phone
    
    1. In game, press F8 → run nui_devTools → open http://localhost:13172 and pick your NUI page.
    2. Inspect network tab. Every NUI → Lua call hits https://my_phone/<name> endpoints.
    3. Use /myphone command and confirm focus toggles.
    4. Run Resmon and verify CPU stays low while the phone is open and closed.

    Step 9 — Packaging & updates

    1. Commit ui/ source and ui/dist/ build.
    2. In CI, run pnpm --filter ui build and ship only dist in releases.
    3. Version your SQL migrations. Never drop user data without backups.

    Step 10 — Extensions you can add next

    1. Banking: link to your server’s banking resource; expose balance and transfers.
    2. Tweets/Ads: global feed with rate limits and moderation.
    3. Marketplace: listings with escrow.
    4. Job apps: police/EMS MDT hooks.
    5. Photos: screenshot integration via server endpoint, not data URLs.
    6. 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
      }
    }
    
    -- client
    RegisterNUICallback('contacts:add', function(data, cb)
      lib.callback('my_phone:server:addContact', false, function(resp)
        cb(resp)
      end, data)
    end)
    

    What you built

    • A focused phone MVP with contacts and messages.
    • A clean NUI bridge that works on both frameworks.
    • A DB layer you can expand safely.

    Ship it, measure performance, and iterate.


    Further reading

    • FiveMX Resmon & optimization: https://fivemx.com/blog/how-to-use-resmon-in-fivem-optimize-resources/
    • Voice comparison (choose your stack): https://fivemx.com/blog/fivem-voice-mumble-saltychat-pma-voice-guide
    • Existing phones for inspiration:
    • lb‑phone v2: https://fivemx.com/shop/lb-phone-v2
    • GCPhone: https://fivemx.com/gcphone
    • Z‑Phone: https://fivemx.com/shop

    Meta

    • Target resmon: ≤0.02 ms average while idle.
    • Bundle budget: ≤250 KB gzipped for MVP.
    • UI FPS: 60.

    Related Guides

    Looking for the best resources? Check out our expert guides:

    • ESX vs QBCore vs QBOX Comparison 2026
    • Phone Script Comparison 2026
    • Economy Script Comparison

    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.

    Launch faster

    Compare curated bundles

    Bundles shorten the path from planning to launch by grouping the highest-leverage scripts into a cleaner commercial starting point.

    View bundles
    Development
    build custom phone app (nui react)
    complete FiveM content creation guide
    A running FiveM server
    oxmysql or mysql-async
    Previous Article

    FiveM: How To Play (Full Guide)

    Next Article

    FivePD — The Complete Beginner’s Guide (Setup, Gamepl...

    Best FiveM Phone Scripts 2026: Complete Comparison Guide
    FiveM Phone Scripts Troubleshooting FAQ: 12 Common Issues Fixed
    How to Stream Custom Clothing in FiveM
    Best FiveM Loading Screen Scripts 2026 — Custom Screens That Impress
    How To Install Custom Cars (FiveM)
    Browse QBCore-ready scripts
    Review the ESX script path
    Browse premium FiveM scripts
    ESX Menu Design

    ESX Menu Design

    $5.49
    ESX Inventory HUD V16

    ESX Inventory HUD V16

    $8.49
    ESX Plugin For EasyAdmin

    ESX Plugin For EasyAdmin

    $4.49
    ESX Enhanced Barber

    ESX Enhanced Barber

    $13.49
    Gameconfig for Legacy & Enhanced

    Gameconfig for Legacy & Enhanced

    8,243,364 downloads
    PC Trainer V

    PC Trainer V

    1,272,946 downloads
    LemonUI: Open Source UI Library

    LemonUI: Open Source UI Library

    1,138,096 downloads
    NFS gauge - RPM Gear Speedometer & Timer

    NFS gauge - RPM Gear Speedometer & Timer

    1,058,515 downloads
    SQL & Identifiers Migration: steam/license to citizenid
    SQL & Identifiers Migration: steam/license to citizenid
    How to Create a Tebex Store for FiveM
    How to Create a Tebex Store for FiveM
    How To Create an alt:V Server (2026 Quickstart Guide)
    How To Create an alt:V Server (2026 Quickstart Guide)

    No time to configure everything yourself?

    Start with a pre-built, tested FiveM server pack. Framework-optimized, all scripts pre-installed.

    Super ESX Server
    esxstandalone

    Super ESX Server

    The Super ESX Server is one of the best FiveM server templates - over 1.000 purchases! Want to know why we call it our Super Server? Check out our video to find out some of the basics details of the world. Update 10 is included, make sure to install v7 first and then use content of v10 yo

    $228.32
    ESX Server Base (by RibSosay)
    esxstandalone

    ESX Server Base (by RibSosay)

    Prebuilt FiveM server with ESX framework GUARANTEE : We offer a guarantee ensuring compatibility with your setup.

    $53.99
    View all server packs