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:
| App | Key | Value |
|---|---|---|
| browser | bookmarks | Array of {name, url, emoji} |
| browser | history | Array of {url, title, time} |
| notes | all | Array 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_dataLoad (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) → booleanServer 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
- localStorage is always written first (instant UI)
- MySQL is written async (durable storage)
- On mount, localStorage loads first (no flash)
- MySQL loads async and overrides if different
- If MySQL is unavailable, localStorage provides full functionality