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