Skip to content

Database Sync

How tablet data is persisted to MySQL and loaded on startup.

Database Tables

wtf_tablet_settings

Per-player tablet appearance/layout preferences.

sql
CREATE TABLE wtf_tablet_settings (
    citizenid VARCHAR(50) NOT NULL,
    settings_json LONGTEXT DEFAULT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (citizenid)
);

Stored: All settings from DEFAULT_SETTINGS (theme, colors, font, scale, etc.)

wtf_tablet_installed_apps

Per-player installed app list.

sql
CREATE TABLE wtf_tablet_installed_apps (
    citizenid VARCHAR(50) NOT NULL,
    apps_json LONGTEXT DEFAULT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (citizenid)
);

Stored: Array of app IDs (e.g., ["group_manager","calculator","browser"])

wtf_tablet_app_data

Generic key-value storage per citizen per app.

sql
CREATE TABLE wtf_tablet_app_data (
    id INT AUTO_INCREMENT,
    citizenid VARCHAR(50) NOT NULL,
    app_id VARCHAR(50) NOT NULL,
    data_key VARCHAR(100) NOT NULL,
    data_value LONGTEXT DEFAULT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE INDEX idx_app_data_key (citizenid, app_id, data_key)
);

Used for:

AppKeyValue
browserbookmarksArray of {name, url, emoji}
browserhistoryArray of {url, title, time}
notesallArray of {id, title, text, created}
(any app)(any key)Any JSON data

wtf_tablet_notifications

Notification log (exists but not actively used yet).

sql
CREATE TABLE wtf_tablet_notifications (
    id INT AUTO_INCREMENT,
    citizenid VARCHAR(50) NOT NULL,
    app_id VARCHAR(50) NOT NULL,
    title VARCHAR(100) DEFAULT NULL,
    message TEXT DEFAULT NULL,
    type VARCHAR(20) DEFAULT 'info',
    is_read TINYINT(1) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
);

Data Flow

Save (UI → DB)

React: saveAppDataToDB('browser', 'bookmarks', data)

dbStorage.js: fetch('https://wtf_group/tablet:setAppData', { appId, key, value })

client/tablet.lua: RegisterNUICallback → TriggerServerEvent

server/tablet.lua: DB.SetAppData(cid, appId, key, value)

server/database.lua: INSERT ... ON DUPLICATE KEY UPDATE

MySQL: wtf_tablet_app_data

Load (DB → UI)

React: loadAppDataFromDB('browser', 'bookmarks')

dbStorage.js: fetch('https://wtf_group/tablet:getAppData') + waitForEvent()

client/tablet.lua: TriggerServerEvent

server/tablet.lua: DB.GetAppData(cid, appId, key) → TriggerClientEvent

client/tablet.lua: SendNUIMessage({ action: 'tablet:appDataLoaded', ... })

React: window.addEventListener('message') → waitForEvent resolves

React: setData(value)

Server Functions

lua
-- Settings
DB.LoadSettings(citizenid) → table|nil
DB.SaveSettings(citizenid, settings) → boolean

-- Installed Apps
DB.LoadInstalledApps(citizenid) → array|nil
DB.SaveInstalledApps(citizenid, apps) → boolean

-- App Data (generic)
DB.SetAppData(citizenid, appId, key, value) → boolean
DB.GetAppData(citizenid, appId, key) → any|nil
DB.DeleteAppData(citizenid, appId, key) → boolean
DB.GetAllAppData(citizenid, appId) → table
DB.ClearAppData(citizenid, appId) → boolean

Server Events

lua
-- Settings
TriggerServerEvent('wtf_group:server:tablet:loadSettings')
TriggerServerEvent('wtf_group:server:tablet:saveSettings', settings)

-- Installed Apps
TriggerServerEvent('wtf_group:server:tablet:loadInstalledApps')
TriggerServerEvent('wtf_group:server:tablet:saveInstalledApps', apps)

-- App Data
TriggerServerEvent('wtf_group:server:tablet:setAppData', appId, key, value)
TriggerServerEvent('wtf_group:server:tablet:getAppData', appId, key)
TriggerServerEvent('wtf_group:server:tablet:getAllAppData', appId)
TriggerServerEvent('wtf_group:server:tablet:deleteAppData', appId, key)

Exports (for other resources)

lua
-- App Data
exports['wtf_group']:SetAppData(citizenid, appId, key, value)
exports['wtf_group']:GetAppData(citizenid, appId, key)
exports['wtf_group']:DeleteAppData(citizenid, appId, key)
exports['wtf_group']:GetAllAppData(citizenid, appId)

Fallback Strategy

  1. localStorage is always written first (instant UI)
  2. MySQL is written async (durable storage)
  3. On mount, localStorage loads first (no flash)
  4. MySQL loads async and overrides if different
  5. If MySQL is unavailable, localStorage provides full functionality

AIFAZI — FiveM Resources