Custom Applications
External applications run from a separate FiveM resource and register with src-phone at runtime. The phone does not compile third-party code into its own React bundle.
Design boundary
The phone shell remains responsible for:
Status bar and Dynamic Island.
Shared application header and back button.
Home gesture and application switcher.
Lock state and phone visibility.
Notifications and network availability checks.
Application viewport clipping and pointer-event control.
The external resource owns only the content rendered below the shared header. It runs in a sandboxed iframe on its own cfx-nui origin. This prevents application CSS and DOM code from modifying the phone shell.
Starting from the boilerplate
Copy src-phone/boilerplate/src-phone-app to a top-level resource directory and rename the folder:
resources/
src-phone-app/
fxmanifest.lua
config.lua
client.lua
web/
blank.html
package.json
tsconfig.json
vite.config.ts
index.html
public/
src/
dist/Keep fxmanifest.lua, config.lua, and client.lua at the resource root. TypeScript, npm, Vite, source assets, and the production bundle belong under web/.
Install and build from the web directory:
Start order:
Why blank.html is the resource UI page
FiveM mounts every resource ui_page as a game-wide NUI surface. If the application uses web/dist/index.html directly as ui_page, its interface appears across the game screen as soon as the resource starts.
Use a transparent page for the automatic surface:
src-phone loads the real uiPage inside the phone and appends srcPhoneEmbedded=1. Keep the entry-page guard supplied by the boilerplate as a second protection:
Do not replace this with window.self !== window.top. FiveM's normal NUI surface already runs inside an iframe, so that check cannot distinguish the phone viewport.
Application config
The root config.lua file is the single source for application metadata. The shell validates this table during registration and sends the accepted values to the TypeScript SDK when the application opens:
Do not duplicate these values in a TypeScript manifest. In particular, permission checks and storage namespacing use the validated runtime config sent by src-phone.
Config fields
apiVersion
Yes
Must be 1. Unknown major versions are rejected.
id
Yes
Unique kebab-case ID, 2-48 characters, beginning with a lowercase letter.
name
Yes
Display name, 1-18 Unicode characters.
uiPage
No
Relative resource path. Defaults to web/dist/index.html. External URLs and .. are rejected.
icon
No
Relative SVG or PNG path. At least 128 by 128 is recommended.
iconClass
No
Font Awesome fallback class. Defaults to fas fa-cube.
accentColor
No
Six-digit hex color. Defaults to #5B8CFF.
permissions
No
v1 allowlist: storage, notify.
requiresInternet
No
When true, the phone blocks launch while offline. Defaults to false.
defaultInstalled
No
Defaults to true. See installation modes below.
Built-in application IDs are reserved. Do not use IDs such as phone, messages, settings, camera, appstore, or garage.
Installation modes
Resource-provided application
The application is installed automatically while its resource is registered. It behaves like a resource-provided system application and cannot be uninstalled from edit mode.
App Store application
The application appears in the phone App Store. Each player installs or removes it through the existing persistent installed-app and home-layout flow.
Stopping the owner resource removes its registration. Starting it again makes it available under the same ID.
Registration
Load config.lua before client.lua in fxmanifest.lua, then pass PhoneAppConfig to the public export:
The second registration path is required because restarting src-phone clears its in-memory registry while the external resource remains running.
TypeScript contract
The boilerplate SDK exposes the application context:
Initialize one SDK instance:
onReady receives the validated id, name, owner resource, accentColor, and permissions from the shell. Do not create one SDK instance per screen. A single instance owns the runtime application metadata, route stack, shell messaging, launch parameters, and lifecycle subscriptions.
Navigation and header
Use SDK navigation instead of creating a second top-level header:
The shell back button sends a back request to the SDK. goBack() pops the application route stack. At the root route it closes the application and returns to the phone home screen.
Set the shared header title when a route changes:
The shell truncates title input to 32 characters. Do not render a duplicate fixed header inside the content viewport.
Launch parameters
Read parameters passed when the shell opens the application:
Treat all launch parameters as untrusted input. Validate types and identifiers before using them in a resource callback.
Lifecycle
Applications remain mounted for switching and recent-app behavior. Pause work when inactive:
Stop polling, timers, expensive animation, camera access, and continuous audio when active is false. Background applications do not receive pointer events.
Runtime events from Lua
Client or server integrations can deliver data to a mounted external app with the public sendAppEvent export. The shell checks the target app ID and forwards the event into that app's iframe; the external app does not listen to raw FiveM window messages.
Client:
Server:
TypeScript:
Validate every payload in TypeScript. Server-authored events still become browser input once they enter the iframe.
Calling the application's resource
The SDK targets the external application's own NUI callbacks, not src-phone:
Register the callback in the external resource:
Every callback path must call cb(...) exactly once. Validate server-owned actions on the server. A NUI payload must never be trusted for money, inventory, permissions, jobs, or ownership.
Storage permission
Declare storage before using the SDK storage wrapper:
Keys are namespaced by application ID and limited to safe characters. Storage is suitable for UI preferences and drafts. Do not store authoritative gameplay state, credentials, or secrets in browser storage.
Notification permission
Declare notify before sending a shell notification:
The phone limits the message length and routes it through the shared notification banner and notification center. External applications do not create global fixed-position toasts.
Theme and layout tokens
The SDK writes shell theme values to CSS custom properties:
Recommended baseline:
16 px horizontal content padding.
8, 12, 16, 24, and 32 px spacing scale.
12 px card radius.
At least 52 px list-row height.
At least 44 by 44 px interaction targets.
20/24 px page headings, 14/20 px body text, and 12/16 px metadata.
Use theme tokens rather than fixed light or dark colors.
UI restrictions
External applications must not:
Access or modify the parent document.
Render a replacement Dynamic Island, status bar, home indicator, or phone header.
Use
position: fixedfor application-wide overlays.Depend on
100vwor100vhfor phone sizing.Use arbitrary global z-index values to escape the application viewport.
Navigate the top frame, open popup windows, or target external form windows.
Call another resource's private NUI endpoint.
Request permissions that the application does not use.
The iframe sandbox is a boundary, not a replacement for server-side authorization.
Custom application release checklist
Use a unique ID and keep it unchanged after release.
Confirm
web/blank.htmlremains theui_page.Run
npm run typecheckandnpm run buildfromweb/.Confirm
web/dist/index.htmland the icon are included infiles.Start the app before and after restarting
src-phone.Test install and uninstall behavior for the selected
defaultInstalledmode.Test root and nested-route back behavior.
Test light and dark themes.
Test offline launch when
requiresInternetis enabled.Test inactive lifecycle behavior in the application switcher.
Test empty, loading, error, and long-content states.
Confirm no application UI appears outside the phone when the resource starts.
See API and Exports for registration errors and return values.
Last updated

