Ready-made starting points for building custom apps on sd-phone. Pick a stack, copy a folder, ship an app.
Each folder in this repository is a complete, standalone FiveM resource that registers the same small Example app on the phone: a live data card fed by fetchNui, a notes field demonstrating keyboard handling, a server push panel, and buttons for popups, context menus and notifications, with automatic light and dark theming.
The five variants are the same app written five ways, so comparing folders shows exactly what changes between stacks and nothing else.
Full documentation: the Custom App Guide covers the registration field table, the injected API surface, messaging patterns, and limits. This README covers the templates themselves.
| Folder | Stack | Build step | Push command |
|---|---|---|---|
sd-template-vanillajs |
Plain HTML/CSS/JS | none | /appmessage |
sd-template-reactjs |
React 18 + Vite (JavaScript) | npm run build |
/appmessage_react |
sd-template-reactts |
React 18 + Vite (TypeScript) | npm run build |
/appmessage_reactts |
sd-template-vuejs |
Vue 3 + Vite | npm run build |
/appmessage_vue |
sd-template-sveltejs |
Svelte 5 + Vite | npm run build |
/appmessage_svelte |
Every variant ships with its UI already built (ui/dist is committed), so all five run straight from a server restart with no npm required. The TypeScript template additionally includes components.d.ts, which types the injected API surface.
- Copy the variant you want into your resources folder and rename it.
- Set your own
Identifier,Name,DescriptionandDeveloperinconfig.lua. ensurethe resource after sd-phone in yourserver.cfg. It then registers itself and re-registers automatically whenever sd-phone restarts.- Open the phone. The app is pre-installed (
DefaultApp = true); set it tofalseto make it an App Store download instead. - Try the push command from the table above, for example
/appmessage hello world, and watch the text arrive in the app.
Start order matters. Your app resource must start after sd-phone. In
server.cfg, put itsensureline below sd-phone's, and never start a custom app before the phone. Starting first can leave the app unregistered until you restart it.
Each variant contains the same three Lua files plus a ui/ folder:
| File | Role |
|---|---|
config.lua |
The values copied into registration: identifier, name, description, developer, install mode |
client.lua |
Registers the app, answers the UI's fetchNui calls, and pushes live messages with sendCustomAppMessage |
fxmanifest.lua |
Whitelists the UI files and declares ui_page, which client.lua reads back to build the registration URL |
ui/ |
The app webpage. Vite variants keep source in ui/src and the committed build in ui/dist |
These two conventions are the difference between an app that works and an app that paints your screen or renders blank. Both are already wired into every variant.
1. Keep the body hidden and transparent by default
FiveM renders every resource's ui_page as a fullscreen overlay in the game at all times. Your stylesheet must hide it; the phone reveals the page when it loads inside the app frame, and the dev-mode branch reveals it in a plain browser.
html, body {
visibility: hidden;
background: transparent;
}Backgrounds need the second line because of a CSS quirk: a background color on html or body is painted onto the page canvas even while the element is hidden, which floods the whole game screen with that color. Keep the body transparent and put your background color on a wrapper element instead; the templates use the #root / #app mount node.
In game, sd-phone injects the API globals (fetchNui, useNuiEvent, SetPopUp, GetSettings, and the rest) into your page, then posts the string componentsLoaded to your window. Rendering earlier means the globals do not exist yet.
const devMode = !window.invokeNative;
if (window.name === '' || devMode) {
if (devMode) {
document.body.style.visibility = 'visible';
render();
} else {
window.addEventListener('message', (e) => {
if (e.data === 'componentsLoaded') render();
});
}
}window.invokeNative only exists inside FiveM, which makes it the dev-mode detector: in a plain browser the app renders immediately with mocked data, so you can develop without the game running.
- In the variant's
ui/folder runnpm install, thennpm run dev(Vite serves onhttp://localhost:5173). - In
fxmanifest.lua, swapui_pageto the commentedhttp://localhost:5173line and restart the resource. - The phone now loads your dev server inside the app frame and edits hot-reload. Swap back and
npm run buildfor production.
The Vite variants keep package-lock.json but not node_modules; run npm install once before building. Use rem and em units in your CSS so the app stays crisp if you enable fixBlur in the registration.
sd-phone ships a drop-in lb-phone compatibility layer, so apps written against exports['lb-phone']:AddCustomApp register and run unmodified, including the injected globals in both capitalizations and the componentsLoaded handshake. Details in the compatibility docs.