Skip to main content
A tablet app is a normal FiveM resource with a small React bundle. The tablet loads that bundle at runtime and mounts it in its own frame, so your resource has no ui_page and never touches NUI focus. The tablet owns the device, the status bar, the dock and notifications. You own what is inside your app.
This page is for developers. If you only want to send a notification, show the HUD or read a player’s group, you do not need an app. See Exports.

What you need

The React version is not a suggestion. The tablet shares its copy of React with every app. If your app ships a different major, the page ends up with two copies and every hook in your app fails with “Invalid hook call”.
Your app also needs two packages from us: @awoken/tablet-sdk for hooks, and @awoken/tablet-design-system for components that match the tablet. They are not part of the release download. Ask for them through support and reference them from your package.json as local folders:
package.json

Quick start

1

Register the app from your server

server.lua
The ?v= on the end matters. See Restarting does not update the UI.
2

Serve the bundle from your manifest

fxmanifest.lua
No ui_page.
3

Configure Vite as a federation remote

web/vite.config.ts
Keep assetsDir: ''. With Vite’s default, the bundle looks for its chunks under assets/assets/ and they fail to load.
4

Write the component

web/src/MyApp.tsx
The component must be the file’s default export.
5

Build and restart

Build the web project, restart your resource, and open the tablet. Your tile is on the home screen.

Registering

RegisterApp returns ok, err. It fails when a required field is missing (id, name, remoteEntry, moduleName), when the id is already registered by another resource, or when sdkVersion does not match the tablet’s. The tablet drops your app on its own when your resource stops, so you do not need to deregister on shutdown. DeregisterApp(id) is only for hiding an app at runtime. The tablet’s SDK version is 1. An app on a different major is refused and its tile reads “Update”. Only raise sdkVersion when we announce a new major.

Who can see the app

Pass a visibility list to RegisterApp. Every rule must pass. Leave it out and everyone sees the app.
Rules are re-checked on player load, inventory change, and job or gang change. If something else changed, such as a flag your own script keeps, call:
Use sdcard for any app you want locked behind a physical unlock. It works whichever way the server owner has SD cards set, so you never have to know.

Talking to your server

Register an ox_lib callback and call it from React.
To push something to an open app, use SendAppEvent:
Delivery is best effort. A closed tablet drops the message, so your app must rebuild its state from a callback every time it opens. For anything the player must not miss, use PushNotification.
Never send a vector to the tablet. Strings, numbers, booleans and tables of those only. A vector3 anywhere in a payload crashes the player’s game outright, with no Lua error to point at the cause.SendAppEvent checks for this, refuses the payload and prints the offending key. Callback returns have no such check. Send coordinates as plain { x, y, z } tables.

The SDK

The design system exports Button, Card, FieldRow, SectionHeader and Select, plus CSS variables such as --bg-panel, --text-primary and --awoken-primary.

Translations

The active language follows the tablet’s Config.Locale. A missing key falls back to English, then to the key itself.

If your app crashes

Each app runs inside its own error boundary. If your component throws while rendering, the player sees a “could not load” message inside your app’s frame and the rest of the tablet keeps working. The error is logged to the F8 console. Errors inside promises and event handlers are not caught by it. Handle those yourself.

Common pitfalls

The tablet caches a loaded bundle against its URL. Register the same URL twice and it keeps the one it already has.Put a token on your remoteEntry URL that changes whenever your resource starts, as in the quick start. Version first, then the start time.
The bundle could not be fetched. Check the remoteEntry URL is https://cfx-nui-<your-resource>/web/dist/remoteEntry.js with the resource name matching the folder name exactly, and that your manifest’s files block includes web/dist/**/*.
Set assetsDir: '' in your Vite config.
Your app loaded its own copy of React. Pin react and react-dom to ^19.0.0 and list both in the federation shared array.
The file you expose must export default your component. Avoid named exports alongside it in that file.
A vector reached the tablet, most often coordinates in a callback return. Convert them to plain { x, y, z } tables before returning.
oxmysql returns TIMESTAMP columns as numbers. Format them in the query so the app always receives a string: