{"id":199625,"date":"2025-10-04T16:06:10","date_gmt":"2025-10-04T14:06:10","guid":{"rendered":"https:\/\/fivemx.com\/?p=199625"},"modified":"2026-07-31T07:28:00","modified_gmt":"2026-07-31T05:28:00","slug":"erstellen-sie-eine-benutzerdefinierte-telefon-app","status":"publish","type":"post","link":"https:\/\/fivemx.com\/de\/erstellen-sie-eine-benutzerdefinierte-telefon-app\/","title":{"rendered":"Baue eine benutzerdefinierte FiveM-Handy-App"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Goal<\/strong><br>Create a production\u2011ready in\u2011game 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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><a href=\"https:\/\/fivemx.com\/how-to-create-a-fivem-server\/\">A running FiveM server<\/a> with txAdmin and MySQL (<a href=\"https:\/\/fivemx.com\/mysql-async-to-oxmysql\/\" data-type=\"post\" data-id=\"193033\">oxmysql or mysql-async<\/a>).<\/li>\n\n\n\n<li>Node.js 18+ and pnpm or npm on your dev PC.<\/li>\n\n\n\n<li>One framework installed: <strong>QBCore<\/strong> or <strong>ESX<\/strong>.<\/li>\n\n\n\n<li>Recommended libs: <strong>ox_lib<\/strong> (callbacks, notifications), <strong>ox_inventory<\/strong> (optional for phone item), <strong>ox_target<\/strong> (optional for world interactions).<\/li>\n\n\n\n<li>Basic React knowledge.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Docs<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Cfx.re NUI overview \u2013 <a href=\"https:\/\/docs.fivem.net\/docs\/scripting-manual\/nui-development\/\" target=\"_blank\" rel=\"noopener\">https:\/\/docs.fivem.net\/docs\/scripting-manual\/nui-development\/<\/a><\/li>\n\n\n\n<li>NUI callbacks \u2013 <a href=\"https:\/\/docs.fivem.net\/docs\/scripting-manual\/nui-development\/nui-callbacks\/\" target=\"_blank\" rel=\"noopener\">https:\/\/docs.fivem.net\/docs\/scripting-manual\/nui-development\/nui-callbacks\/<\/a><\/li>\n\n\n\n<li>SendNUIMessage \u2013 <a href=\"https:\/\/docs.fivem.net\/docs\/scripting-reference\/runtimes\/lua\/functions\/SendNUIMessage\/\" target=\"_blank\" rel=\"noopener\">https:\/\/docs.fivem.net\/docs\/scripting-reference\/runtimes\/lua\/functions\/SendNUIMessage\/<\/a><\/li>\n\n\n\n<li>Debug NUI devtools \u2013 <a href=\"https:\/\/docs.fivem.net\/docs\/scripting-manual\/nui-development\/full-screen-nui\/\" target=\"_blank\" rel=\"noopener\">https:\/\/docs.fivem.net\/docs\/scripting-manual\/nui-development\/full-screen-nui\/<\/a><\/li>\n\n\n\n<li>QBCore server functions \u2013 <a href=\"https:\/\/docs.qbcore.org\/qbcore-documentation\/qb-core\/server-function-reference\" target=\"_blank\" rel=\"noopener\">https:\/\/docs.qbcore.org\/qbcore-documentation\/qb-core\/server-function-reference<\/a><\/li>\n\n\n\n<li>ESX <code>RegisterUsableItem<\/code> \u2013 <a href=\"https:\/\/docs.esx-framework.org\/en\/esx_core\/es_extended\/server\/functions\" target=\"_blank\" rel=\"noopener\">https:\/\/docs.esx-framework.org\/en\/esx_core\/es_extended\/server\/functions<\/a><\/li>\n\n\n\n<li>ox_lib callbacks \u2013 <a href=\"https:\/\/overextended.dev\/ox_lib\" target=\"_blank\" rel=\"noopener\">https:\/\/overextended.dev\/ox_lib<\/a><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Internal reading (FiveMX)<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Resmon &amp; performance \u2013 <a href=\"https:\/\/fivemx.com\/how-to-use-resmon-in-fivem-optimize-resources\/\">https:\/\/fivemx.com\/how-to-use-resmon-in-fivem-optimize-resources\/<\/a><\/li>\n\n\n\n<li>Performance hub \u2013 <a href=\"https:\/\/fivemx.com\/performance\/\">https:\/\/fivemx.com\/performance\/<\/a><\/li>\n\n\n\n<li>Phone scripts market overview \u2013 <a href=\"https:\/\/fivemx.com\/phone-scripts\/\">https:\/\/fivemx.com\/phone-scripts\/<\/a><\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Architecture<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Resource<\/strong> <code>my_phone<\/code> with <code>fxmanifest.lua<\/code>, <code>client<\/code>, <code>server<\/code>, and <code>ui<\/code> bundle.<\/li>\n\n\n\n<li><strong>UI<\/strong>: React app built with Vite into <code>\/ui\/dist<\/code>. NUI talks to Lua via <code>postMessage<\/code> + <code>RegisterNUICallback<\/code>.<\/li>\n\n\n\n<li><strong>Data<\/strong>: MySQL tables for <code>phone_contacts<\/code>, <code>phone_messages<\/code>, <code>phone_calls<\/code>.<\/li>\n\n\n\n<li><strong>Framework glue<\/strong>: QBCore <strong>or<\/strong> ESX item usable handler toggles the phone, and server callbacks load\/save data.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Event flow<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Player presses key or uses the phone item \u2192 2) <code>SetNuiFocus(true, true)<\/code> and <code>SendNUIMessage({ action = 'open' })<\/code> \u2192 3) React shows UI \u2192 4) UI requests data via <code>fetch('https:\/\/my_phone\/xyz')<\/code> (NUI) \u2192 5) <code>RegisterNUICallback('xyz', ...)<\/code> runs on client\/server \u2192 6) Server reads\/writes DB \u2192 7) Response returns to UI \u2192 8) Close phone and release focus.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1 \u2014 Scaffold the resource<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder layout<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">resources\/\n [local]\/\n my_phone\/\n fxmanifest.lua\n client\/\n main.lua\n server\/\n main.lua\n ui\/\n index.html\n src\/\n main.tsx\n App.tsx\n api.ts\n styles.css\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>fxmanifest.lua<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">fx_version 'cerulean'\ngame 'gta5'\n\nui_page 'ui\/dist\/index.html'\n\nfiles { 'ui\/dist\/**' }\n\nclient_scripts { 'client\/main.lua' }\nserver_scripts {\n '@oxmysql\/lib\/MySQL.lua',\n 'server\/main.lua'\n}\n\nlua54 'yes'\n<\/pre>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/fivemx.com\/setting-up-fxmanifest-lua-fivem\/\">More info about: fxmanifest.lua<\/a><\/div>\n<\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2 \u2014 Create the React NUI<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Initialize a Vite React app inside <code>my_phone\/ui<\/code> and build to <code>ui\/dist<\/code>.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">cd my_phone\/ui\npnpm create vite@latest . --template react-ts\npnpm i\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Vite config<\/strong> (ensure assets land in <code>dist<\/code>)<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ vite.config.ts\nimport { defineConfig } from 'vite'\nimport react from '@vitejs\/plugin-react'\nexport default defineConfig({\n plugins: [react()],\n build: { outDir: 'dist', emptyOutDir: true },\n base: ''\n})\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>NUI bridge<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ src\/api.ts\nexport async function nui&lt;T&gt;(event: string, data?: unknown): Promise&lt;T&gt; {\n const res = await fetch(`https:\/\/my_phone\/${event}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application\/json' },\n body: JSON.stringify(data ?? {})\n })\n return await res.json()\n}\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Mount React + message listener<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ src\/main.tsx\nimport React from 'react'\nimport ReactDOM from 'react-dom\/client'\nimport App from '.\/App'\n\nconst root = ReactDOM.createRoot(document.getElementById('root')!)\nroot.render(&lt;App \/&gt;)\n\nwindow.addEventListener('message', (e) =&gt; {\n if (e.data?.action === 'open') document.body.classList.add('open')\n if (e.data?.action === 'close') document.body.classList.remove('open')\n})\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Basic UI<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ src\/App.tsx\nimport { useEffect, useState } from 'react'\nimport { nui } from '.\/api'\n\ntype Contact = { id: number; name: string; number: string }\n\nexport default function App() {\n const [contacts, setContacts] = useState&lt;Contact[]&gt;([])\n const [visible, setVisible] = useState(false)\n\n useEffect(() =&gt; {\n const handler = (e: MessageEvent) =&gt; {\n if (e.data?.action === 'open') {\n setVisible(true)\n nui&lt;Contact[]&gt;('contacts:list').then(setContacts)\n }\n if (e.data?.action === 'close') setVisible(false)\n }\n window.addEventListener('message', handler)\n return () =&gt; window.removeEventListener('message', handler)\n }, [])\n\n if (!visible) return null\n return (\n &lt;div className=\"phone\"&gt;\n &lt;header&gt;Phone&lt;\/header&gt;\n &lt;section&gt;\n {contacts.map(c =&gt; (\n &lt;div key={c.id} className=\"row\"&gt;\n &lt;div&gt;{c.name}&lt;\/div&gt;\n &lt;div&gt;{c.number}&lt;\/div&gt;\n &lt;\/div&gt;\n ))}\n &lt;\/section&gt;\n &lt;footer&gt;\n &lt;button onClick={() =&gt; nui('ui:close')}&gt;Close&lt;\/button&gt;\n &lt;\/footer&gt;\n &lt;\/div&gt;\n )\n}\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>index.html<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;!doctype html&gt;\n&lt;html&gt;\n &lt;head&gt;\n &lt;meta charset=\"utf-8\" \/&gt;\n &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/&gt;\n &lt;title&gt;my_phone&lt;\/title&gt;\n &lt;link rel=\"stylesheet\" href=\"\/src\/styles.css\" \/&gt;\n &lt;\/head&gt;\n &lt;body&gt;\n &lt;div id=\"root\"&gt;&lt;\/div&gt;\n &lt;script type=\"module\" src=\"\/src\/main.tsx\"&gt;&lt;\/script&gt;\n &lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Build the UI:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">pnpm build\n<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3 \u2014 Client: open\/close, NUI focus, callbacks<\/h2>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- client\/main.lua\nlocal open = false\n\nlocal function openPhone()\n if open then return end\n open = true\n SetNuiFocus(true, true)\n SendNUIMessage({ action = 'open' })\nend\n\nlocal function closePhone()\n if not open then return end\n open = false\n SetNuiFocus(false, false)\n SendNUIMessage({ action = 'close' })\nend\n\n-- Keybind (F1 example)\nRegisterCommand('myphone', function()\n if open then closePhone() else openPhone() end\nend)\nRegisterKeyMapping('myphone', 'Toggle Phone', 'keyboard', 'F1')\n\n-- NUI \u2192 game callbacks\nRegisterNUICallback('ui:close', function(_, cb)\n closePhone()\n cb({ ok = true })\nend)\n\n-- list contacts asks the server\nRegisterNUICallback('contacts:list', function(_, cb)\n lib.callback('my_phone:server:getContacts', false, function(rows)\n cb(rows)\n end)\nend)\n<\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Tip: enable NUI devtools in game console with <code>nui_devTools<\/code>. Open <code>http:\/\/localhost:13172<\/code> in your Chromium browser to inspect the UI.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4 \u2014 Server: DB schema + callbacks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SQL<\/strong> (MySQL)<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">CREATE TABLE IF NOT EXISTS phone_contacts (\n id INT AUTO_INCREMENT PRIMARY KEY,\n citizenid VARCHAR(64) NOT NULL,\n name VARCHAR(64) NOT NULL,\n number VARCHAR(32) NOT NULL,\n INDEX(citizenid)\n);\n\nCREATE TABLE IF NOT EXISTS phone_messages (\n id BIGINT AUTO_INCREMENT PRIMARY KEY,\n owner VARCHAR(64) NOT NULL,\n peer VARCHAR(64) NOT NULL,\n body TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n INDEX(owner), INDEX(peer)\n);\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Server with oxmysql + ox_lib<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- server\/main.lua\nlocal QBCore = exports['qb-core'] and exports['qb-core']:GetCoreObject()\nESX = ESX or nil\n\nif not QBCore then\n TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)\nend\n\n-- Load contacts for the logged-in character\nlib.callback.register('my_phone:server:getContacts', function(source)\n local citizenid\n if QBCore then\n local Player = QBCore.Functions.GetPlayer(source)\n citizenid = Player and Player.PlayerData.citizenid\n else\n local xPlayer = ESX.GetPlayerFromId(source)\n citizenid = xPlayer and xPlayer.identifier\n end\n if not citizenid then return {} end\n\n local rows = MySQL.query.await('SELECT id, name, number FROM phone_contacts WHERE citizenid = ?', { citizenid })\n return rows or {}\nend)\n\n-- Save a contact\nlib.callback.register('my_phone:server:addContact', function(source, contact)\n if type(contact) ~= 'table' then return { ok = false } end\n local name, number = contact.name, contact.number\n if not name or not number then return { ok = false } end\n\n local citizenid\n if QBCore then\n local Player = QBCore.Functions.GetPlayer(source)\n citizenid = Player and Player.PlayerData.citizenid\n else\n local xPlayer = ESX.GetPlayerFromId(source)\n citizenid = xPlayer and xPlayer.identifier\n end\n if not citizenid then return { ok = false } end\n\n MySQL.insert.await('INSERT INTO phone_contacts (citizenid, name, number) VALUES (?, ?, ?)', { citizenid, name, number })\n return { ok = true }\nend)\n<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5 \u2014 Framework integration (item + permissions)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">QBCore<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Add a usable phone item and toggle the UI when used.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- server\/main.lua (QBCore only)\nif QBCore then\n QBCore.Functions.CreateUseableItem('phone', function(src, item)\n TriggerClientEvent('my_phone:client:toggle', src)\n end)\nend\n<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- client\/main.lua\nRegisterNetEvent('my_phone:client:toggle', function()\n if IsPauseMenuActive() then return end\n if IsPedInAnyVehicle(PlayerPedId(), false) then -- optional rule\n -- show a notification via ox_lib\n lib.notify({ title = 'Phone', description = 'No phone while driving.', type = 'error' })\n return\n end\n if IsNuiFocused() then ExecuteCommand('myphone') else ExecuteCommand('myphone') end\nend)\n<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">ESX<\/h3>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- server\/main.lua (ESX only)\nif ESX and not QBCore then\n ESX.RegisterUsableItem('phone', function(playerId)\n TriggerClientEvent('my_phone:client:toggle', playerId)\n end)\nend\n<\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">If you use <strong>ox_inventory<\/strong>, create the item there and rely on its usable handlers. You can still trigger the same client event.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6 \u2014 Core features<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Implement small slices and ship incrementally.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Contacts<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>UI calls <code>contacts:list<\/code> \u2192 server returns rows.<\/li>\n\n\n\n<li>Add \u201cAdd contact\u201d form \u2192 call <code>addContact<\/code>.<\/li>\n\n\n\n<li>Add \u201cRemove contact\u201d \u2192 server deletes by <code>id<\/code> with citizen ownership check.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Messages (SMS)<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Table <code>phone_messages<\/code> stores owner, peer, body.<\/li>\n\n\n\n<li>UI opens a chat, calls <code>messages:list<\/code> and <code>messages:send<\/code>.<\/li>\n\n\n\n<li>Server inserts message, optionally emits client event to peer if online.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Server sketch<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">lib.callback.register('my_phone:server:messages:list', function(source, peer)\n local cid = GetCitizenId(source)\n return MySQL.query.await('SELECT * FROM phone_messages WHERE owner=? AND peer=? ORDER BY id DESC LIMIT 200', { cid, peer }) or {}\nend)\n\nRegisterNetEvent('my_phone:server:messages:send', function(peer, body)\n local src = source\n local cid = GetCitizenId(src)\n if type(body) ~= 'string' or #body == 0 or #body &gt; 500 then return end\n MySQL.insert.await('INSERT INTO phone_messages (owner, peer, body) VALUES (?, ?, ?)', { cid, peer, body })\n TriggerClientEvent('my_phone:client:messages:push', src, peer, body)\n -- optional: find target player by phone number and push live event\nend)\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Client receive<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">RegisterNetEvent('my_phone:client:messages:push', function(peer, body)\n SendNUIMessage({ action = 'message:new', peer = peer, body = body })\nend)\n<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Calls (optional MVP)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Store call logs only. Real audio uses your voice plugin (pma-voice, mumble, SaltyChat) and is outside this MVP.<\/li>\n\n\n\n<li>Add UI keypad \u2192 on dial, log an outgoing call; on answer, log incoming. You can integrate later with a voice plugin\u2019s API.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 7 \u2014 Security, UX, performance<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Security<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Never trust NUI input. Validate types and length on server.<\/li>\n\n\n\n<li>Check ownership on every query with <code>citizenid<\/code> or <code>identifier<\/code>.<\/li>\n\n\n\n<li>Avoid exposing identifiers to other clients. Use server relays.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>UX<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Cancel phone while downed, cuffed, or driving, if your server rules require it.<\/li>\n\n\n\n<li>Keep UI snappy. Use optimistic updates and reconcile on server ack.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Performance<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Keep NUI idle. Avoid setInterval loops in React. Use effects and events.<\/li>\n\n\n\n<li>Keep bundles small. Lazy\u2011load heavy screens. Ship compressed assets.<\/li>\n\n\n\n<li>Use Resmon to budget under 0.01\u20130.02 ms on average. See FiveMX guide linked above.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 8 \u2014 Testing &amp; debugging<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Start resource in <code>server.cfg<\/code> before dependent scripts.<\/li>\n<\/ol>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">ensure my_phone\n<\/pre>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li>In game, press F8 \u2192 run <code>nui_devTools<\/code> \u2192 open <code>http:\/\/localhost:13172<\/code> and pick your NUI page.<\/li>\n\n\n\n<li>Inspect network tab. Every NUI \u2192 Lua call hits <code>https:\/\/my_phone\/&lt;name&gt;<\/code> endpoints.<\/li>\n\n\n\n<li>Use <code>\/myphone<\/code> command and confirm focus toggles.<\/li>\n\n\n\n<li>Run Resmon and verify CPU stays low while the phone is open and closed.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 9 \u2014 Packaging &amp; updates<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Commit <code>ui\/<\/code> source and <code>ui\/dist\/<\/code> build.<\/li>\n\n\n\n<li>In CI, run <code>pnpm --filter ui build<\/code> and ship only <code>dist<\/code> in releases.<\/li>\n\n\n\n<li>Version your SQL migrations. Never drop user data without backups.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 10 \u2014 Extensions you can add next<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Banking<\/strong>: link to your server\u2019s banking resource; expose balance and transfers.<\/li>\n\n\n\n<li><strong>Tweets\/Ads<\/strong>: global feed with rate limits and moderation.<\/li>\n\n\n\n<li><strong>Marketplace<\/strong>: listings with escrow.<\/li>\n\n\n\n<li><strong>Job apps<\/strong>: police\/EMS MDT hooks.<\/li>\n\n\n\n<li><strong>Photos<\/strong>: screenshot integration via server endpoint, not data URLs.<\/li>\n\n\n\n<li><strong>Settings<\/strong>: dynamic themes, ringtones, backgrounds.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Phone opens behind pause menu<\/strong><br>Disable during pause checks and reopen when active returns.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>NUI callback not firing<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ensure <code>RegisterNUICallback('event', ...)<\/code> names match the UI fetch path.<\/li>\n\n\n\n<li>Confirm <code>fx_version<\/code> is <code>cerulean<\/code> and <code>ui_page<\/code> points to <code>ui\/dist\/index.html<\/code>.<\/li>\n\n\n\n<li>Check F8 console for CORS or JSON errors.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Items not usable<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>QBCore: confirm <code>QBCore.Functions.CreateUseableItem<\/code> runs and <code>phone<\/code> exists in your item list.<\/li>\n\n\n\n<li>ESX: confirm <code>ESX.RegisterUsableItem('phone', ...)<\/code> registers after inventory loads.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Database errors<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ensure oxmysql started before this resource.<\/li>\n\n\n\n<li>Check column sizes and encodings for Unicode names.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Reference snippets (copy\u2011paste)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Helper<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function GetCitizenId(source)\n if QBCore then\n local P = QBCore.Functions.GetPlayer(source)\n return P and P.PlayerData.citizenid\n else\n local xP = ESX.GetPlayerFromId(source)\n return xP and xP.identifier\n end\nend\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Add contact from UI<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ UI\nasync function addContact(name: string, number: string) {\n const res = await nui&lt;{ ok: boolean }&gt;('contacts:add', { name, number })\n if (res.ok) {\n const next = await nui&lt;any[]&gt;('contacts:list')\n \/\/ update state\n }\n}\n<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-- client\nRegisterNUICallback('contacts:add', function(data, cb)\n lib.callback('my_phone:server:addContact', false, function(resp)\n cb(resp)\n end, data)\nend)\n<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">What you built<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A focused phone MVP with contacts and messages.<\/li>\n\n\n\n<li>A clean NUI bridge that works on both frameworks.<\/li>\n\n\n\n<li>A DB layer you can expand safely.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Ship it, measure performance, and iterate.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Further reading<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>FiveMX Resmon &amp; optimization: <a href=\"https:\/\/fivemx.com\/how-to-use-resmon-in-fivem-optimize-resources\/\">https:\/\/fivemx.com\/how-to-use-resmon-in-fivem-optimize-resources\/<\/a><\/li>\n\n\n\n<li>Voice comparison (choose your stack): <a href=\"https:\/\/fivemx.com\/fivem-voice-mumble-saltychat-pma-voice-guide\/\">https:\/\/fivemx.com\/fivem-voice-mumble-saltychat-pma-voice-guide\/<\/a><\/li>\n\n\n\n<li>Existing phones for inspiration:\n<ul class=\"wp-block-list\">\n<li>lb\u2011phone v2: <a href=\"https:\/\/fivemx.com\/lb-phone-v2\/\">https:\/\/fivemx.com\/lb-phone-v2\/<\/a><\/li>\n\n\n\n<li>Quasar Smartphone: <a href=\"https:\/\/fivemx.com\/phone-scripts\/\">https:\/\/fivemx.com\/phone-scripts\/<\/a><\/li>\n\n\n\n<li>GCPhone: <a href=\"https:\/\/fivemx.com\/free\/gcphone\/\">https:\/\/fivemx.com\/free\/gcphone\/<\/a><\/li>\n\n\n\n<li>Z\u2011Phone: <a href=\"https:\/\/fivemx.com\/free\/z-phone\/\">https:\/\/fivemx.com\/free\/z-phone\/<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Meta<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Target resmon: \u22640.02 ms average while idle.<\/li>\n\n\n\n<li>Bundle budget: \u2264250 KB gzipped for MVP.<\/li>\n\n\n\n<li>UI FPS: 60.<\/li>\n<\/ul>\n\n\n<!-- fivemx-seo-funnel:start -->\n<div class=\"fivemx-content-links\">\n<h2>Related FiveMX resources<\/h2>\n<p>Use these sections to compare related FiveM categories and setup resources before changing a live server.<\/p>\n<ul>\n<li><a href=\"https:\/\/fivemx.com\/phone-scripts\/\">Phone Scripts<\/a><\/li>\n<li><a href=\"https:\/\/fivemx.com\/tools-ui\/\">UI Scripts<\/a><\/li>\n<li><a href=\"https:\/\/fivemx.com\/qbcore-scripts\/\">QBCore Scripts<\/a><\/li>\n<li><a href=\"https:\/\/fivemx.com\/esx-scripts\/\">ESX Scripts<\/a><\/li>\n<\/ul>\n<\/div>\n<!-- fivemx-seo-funnel:end -->","protected":false},"excerpt":{"rendered":"<p>GoalCreate a production\u2011ready in\u2011game 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. Prerequisites Docs Internal reading (FiveMX) Architecture Event flow Step 1 \u2014 Scaffold the resource Folder layout fxmanifest.lua Step 2 \u2014 Create the React [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":199627,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2340,1899],"tags":[3001],"class_list":["post-199625","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-lua-scripting","category-tutorials","tag-fivem-script"],"_links":{"self":[{"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/posts\/199625","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/comments?post=199625"}],"version-history":[{"count":5,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/posts\/199625\/revisions"}],"predecessor-version":[{"id":214898,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/posts\/199625\/revisions\/214898"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/media\/199627"}],"wp:attachment":[{"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/media?parent=199625"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/categories?post=199625"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fivemx.com\/de\/wp-json\/wp\/v2\/tags?post=199625"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}