F5 StudioF5 Studio
Skip to main content

Database

Everything a player builds is stored server-side: their crosshair, effects and scoreboard settings, their uploaded image, their named profiles, their kills and deaths, their marketplace listings and any ban against them.

The Key

Every row is keyed by the player's native FiveM license identifier — the raw license: string, not a QBCore citizenid or an ESX identifier:

license:110000112345678

The bridge takes license, falls back to license2, and only then to the framework's own field. See Framework Compatibility → The save identifier.

Anything that changes the identifier orphans the data

Rows are found by exact string match. If the identifier a player presents ever changes — a custom adapter with its own identifierOf, an identifier-rewriting plugin, or a client that suddenly has no license and falls through to license2 — every existing row for that player becomes unreachable. They see a fresh, default HUD; nothing is deleted, but nothing is found either.

Automatic Install and Repair

config/main.lua
F5Cfg.AutoInstallDatabase = true

With this on (the default) the resource reads install.sql at startup, compares it against your database, then:

  • creates any table that is missing, by replaying install.sql statement by statement;
  • adds any column a table does not have yet, one ALTER TABLE … ADD COLUMN at a time.

It never drops, renames or retypes anything, so it is safe to leave on.

SituationWhat happens
Fresh databaseAll eight tables and their indexes are created
Table exists, column missingThe column is added
Table exists, index missingNothing — indexes only ever arrive with a freshly created table
Still incomplete after repairThe resource stops itself (see below)

Repair gets exactly one pass. If the schema is still incomplete afterwards, the resource does not run on with nothing being saved:

[f5_combathud] DATABASE SCHEMA INCOMPLETE — the resource will not run.
[f5_combathud] Missing tables: f5_combathud_marketplace
[f5_combathud] Missing columns: f5_combathud_player_crosshairs.last_seen
[f5_combathud] Automatic repair ran but the schema is still incomplete. Check the
[f5_combathud] SQL errors above — the database user may lack CREATE/ALTER rights.
[f5_combathud] Stopping the resource so this is not mistaken for a working install.

That is deliberate: left running, the menu would open, the crosshair would work off client state, and every write would quietly return early — players would lose everything at the next relog with nothing to explain why.

Manual install

config/main.lua
F5Cfg.AutoInstallDatabase = false

Then import install.sql yourself. The startup check still runs — a database that is missing something still stops the resource, with a hint pointing at the file:

[f5_combathud] F5Cfg.AutoInstallDatabase is false — import install.sql yourself,
[f5_combathud] or set it to true to let the resource create and repair the schema.
Do not reformat install.sql

The resource parses that file itself, line by line, to learn what the schema should look like. It expects one column per line inside CREATE TABLE IF NOT EXISTS, and table-level clauses (PRIMARY KEY, INDEX, UNIQUE, CONSTRAINT) on their own lines. Reflow it and the columns it can no longer see simply drop out of the expected set — the check then declares an incomplete database healthy. Table order matters too: f5_combathud_marketplace must be created before the tables that reference it.

Tables

Eight tables, all InnoDB / utf8mb4_unicode_ci.

f5_combathud_player_crosshairs

One row per player — the live HUD.

ColumnTypeContents
licenseVARCHAR(64) PKThe identifier above
settings_dataMEDIUMTEXTEvery section as JSON, without the uploaded picture
custom_image_dataMEDIUMTEXTThe uploaded image as a base64 data URL, kept out of the settings blob so a login does not have to fetch it
kills / deathsINT UNSIGNEDK/D counters
active_profileVARCHAR(50)Name of the profile the player is on, default Default
updated_atTIMESTAMPBumped on write
last_seenTIMESTAMP NULLSet on every data load — this is what retention measures inactivity against

f5_combathud_player_crosshair_profiles

One row per named profile, unique per (license, profile_name).

ColumnTypeContents
idINT PK
licenseVARCHAR(64)Owner
profile_nameVARCHAR(50)Unique per player
profile_dataMEDIUMTEXTThe profile's settings, written with stable key ordering
custom_image_dataMEDIUMTEXTThe image belonging to that profile
created_at / updated_atTIMESTAMPupdated_at only moves when something actually changed

f5_combathud_marketplace and friends

TableContents
f5_combathud_marketplacePublished listings — owner, title, description, tags, the full crosshair as JSON (image inline), like and apply counters, hidden flag and reason
f5_combathud_marketplace_likesOne row per player per liked listing, cascade-deleted with the listing
f5_combathud_marketplace_appliesOne row per player per applied listing, cascade-deleted with the listing
f5_combathud_marketplace_banslicense PK, reason, who banned, and expires_atNULL means permanent

See Marketplace.

f5_combathud_admin_audit

ColumnContents
actor_identifierThe moderator's identifier, player:<id>, or system
actionpolicy_strip, rate_limit, marketplace_remove, marketplace_hide, marketplace_unhide, marketplace_ban, marketplace_unban
targetWhat it applied to
old_value / new_valueJSON, where relevant
sourcesave, rate_limit or nui
created_at
The audit table is write-only in game

Nothing in the menu ever reads it — there is no audit viewer. It exists to be queried from outside (phpMyAdmin, a Discord bot, your own panel). If you do not read it externally, leaving audit retention off costs nothing but disk.

f5_combathud_schema_version

A single row holding the schema version (currently 5), written when tables are created. Nothing reads it; it is there so you can tell at a glance which generation of the schema a database was built from.

What a Save Actually Does

A save is one client event carrying the whole settings payload. On the way in:

  1. Rate limit — at most one accepted save per player per 2 seconds. Anything inside that window is dropped outright: no queue, no retry, no error.
  2. Validation — every number is clamped to F5Cfg.Validation, every enum is checked against its whitelist, every colour is normalised. A payload is only rejected wholesale if it is not a table at all; everything else is corrected rather than refused.
  3. Policy — sections that your config has switched off are replaced with the config defaults, and the discarded values are written to the audit table as policy_strip.
  4. Write — the player row is written with INSERT … ON DUPLICATE KEY UPDATE. The uploaded image is only rewritten when it actually changed.
"Settings saved" means the row was written, not that everything got through

The confirmation appears when the write reports at least one affected row. A save that lost the 2-second race never reaches this point and says nothing at all.

Editing in the menu is safe: its autosave paces itself just above the server's window and defers rather than drops. What can be lost is a save triggered outside that pacing — the F9 crosshair toggle, a reset, applying a marketplace crosshair, importing a share code or restoring a backup — when it lands within two seconds of an autosave.

An ordinary save also writes the active profile

Every normal save rewrites the profile row whose name matches active_profile. A player who tweaks a slider "just to try it" updates their saved profile at the same time — there is no separate "unsaved changes" state.

If no profile row of that name exists yet — the initial Default, which is not created until the player saves a profile explicitly — that half of the write simply affects nothing.

What is not saved this way

Kills and deaths never travel in a settings save. They move only through their own events, one increment at a time, and through the player's own K/D reset. A crafted payload cannot set them.

Loading

On character load the server reads the row, revalidates the crosshair, killstreak, K/D HUD, custom image and per-weapon crosshairs, and hands the rest back as stored.

Tightening a limit does not retro-fix stored values

Hitmarker, damage numbers, killmarker, headshot marker, hit direction, kill feed and kill flash are returned exactly as they were stored. If you narrow one of their ranges in config/limits.lua, players who already saved a wider value keep it until they save that section again. Disabled sections are a different matter — those are forced back to defaults on the next save regardless.

If the schema is not ready yet, a load waits up to 15 seconds before giving up:

[f5_combathud] Schema not ready after 15s — request rejected

Retention

Both sweeps are off by default, and both delete rows the moment you turn them on:

config/main.lua
F5Cfg.Retention = {
auditEnabled = false,
auditDays = 30,

playerDataEnabled = false,
playerDataDays = 180,
}
SettingDeletes
auditEnabled / auditDaysAudit rows older than N days
playerDataEnabled / playerDataDaysPlayer rows whose last_seen (or updated_at, when it was never set) is older than N days — plus every profile row whose owner row is gone

Retention runs once, at startup, right after the schema check. There is no timer and no command: a sweep happens on the next restart after you enable it, then again on every restart. It only prints when it actually deleted something:

[f5_combathud] Retention: pruned 412 inactive player rows older than 180 days
[f5_combathud] Retention: pruned 118 orphaned profile rows
There is no dry run and no backup

playerDataEnabled = true deletes settings, profiles and K/D for anyone who has not been seen in playerDataDays days, immediately, on the next restart. Take a database backup before enabling it, and set the day count before, not after.

Marketplace listings, likes, applies and bans are not covered by retention — a purged player's listings stay up. Remove those from the moderation panel.

Full Reset (development only)

install.sql ships with a commented-out block of DROP TABLE statements. Uncommenting it and importing the file destroys every saved crosshair, profile, K/D stat, listing and ban. There is no undo, and no reason to do it on a live server — auto-repair handles ordinary upgrades.

Leave that block commented out

When a whole table is missing, auto-install replays every statement in install.sql, not just the ones for the missing table. If the reset block is uncommented at that moment, the resource runs it — dropping the tables it was about to repair. Uncomment it only for a manual import you are performing deliberately, and comment it out again immediately afterwards.

See Also