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.
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
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.sqlstatement by statement; - adds any column a table does not have yet, one
ALTER TABLE … ADD COLUMNat a time.
It never drops, renames or retypes anything, so it is safe to leave on.
| Situation | What happens |
|---|---|
| Fresh database | All eight tables and their indexes are created |
| Table exists, column missing | The column is added |
| Table exists, index missing | Nothing — indexes only ever arrive with a freshly created table |
| Still incomplete after repair | The 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
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.
install.sqlThe 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.
| Column | Type | Contents |
|---|---|---|
license | VARCHAR(64) PK | The identifier above |
settings_data | MEDIUMTEXT | Every section as JSON, without the uploaded picture |
custom_image_data | MEDIUMTEXT | The uploaded image as a base64 data URL, kept out of the settings blob so a login does not have to fetch it |
kills / deaths | INT UNSIGNED | K/D counters |
active_profile | VARCHAR(50) | Name of the profile the player is on, default Default |
updated_at | TIMESTAMP | Bumped on write |
last_seen | TIMESTAMP NULL | Set 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).
| Column | Type | Contents |
|---|---|---|
id | INT PK | |
license | VARCHAR(64) | Owner |
profile_name | VARCHAR(50) | Unique per player |
profile_data | MEDIUMTEXT | The profile's settings, written with stable key ordering |
custom_image_data | MEDIUMTEXT | The image belonging to that profile |
created_at / updated_at | TIMESTAMP | updated_at only moves when something actually changed |
f5_combathud_marketplace and friends
| Table | Contents |
|---|---|
f5_combathud_marketplace | Published listings — owner, title, description, tags, the full crosshair as JSON (image inline), like and apply counters, hidden flag and reason |
f5_combathud_marketplace_likes | One row per player per liked listing, cascade-deleted with the listing |
f5_combathud_marketplace_applies | One row per player per applied listing, cascade-deleted with the listing |
f5_combathud_marketplace_bans | license PK, reason, who banned, and expires_at — NULL means permanent |
See Marketplace.
f5_combathud_admin_audit
| Column | Contents |
|---|---|
actor_identifier | The moderator's identifier, player:<id>, or system |
action | policy_strip, rate_limit, marketplace_remove, marketplace_hide, marketplace_unhide, marketplace_ban, marketplace_unban |
target | What it applied to |
old_value / new_value | JSON, where relevant |
source | save, rate_limit or nui |
created_at |
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:
- 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.
- 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. - 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. - Write — the player row is written with
INSERT … ON DUPLICATE KEY UPDATE. The uploaded image is only rewritten when it actually changed.
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.
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:
F5Cfg.Retention = {
auditEnabled = false,
auditDays = 30,
playerDataEnabled = false,
playerDataDays = 180,
}
| Setting | Deletes |
|---|---|
auditEnabled / auditDays | Audit rows older than N days |
playerDataEnabled / playerDataDays | Player 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
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.
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
- Installation → Database — first-run setup
- Configuration → Database — the config keys
- Marketplace — the listing tables
- Permissions — what writes to the audit table
- Troubleshooting — "settings are not saving"