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_at — NULL 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. install.sql seeds 6, and the row is written only when a table has to be created — a run that merely adds missing columns never touches it. The seed ends in ON DUPLICATE KEY UPDATE version = version, a deliberate no-op, so a database that was first built under an earlier version keeps that earlier number for good.

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)​

Nothing in the resource ever issues DROP or TRUNCATE, and install.sql ships no reset block — it is eight CREATE TABLE IF NOT EXISTS statements and one seed INSERT, nothing else.

That matters because auto-repair only ever adds. It cannot rebuild a table whose column was renamed or changed type, so a development database that has drifted out of shape has to be dropped by hand. Write the statements yourself, children first so the foreign keys let go:

Run by hand, never from install.sql
DROP TABLE IF EXISTS f5_combathud_marketplace_applies;
DROP TABLE IF EXISTS f5_combathud_marketplace_likes;
DROP TABLE IF EXISTS f5_combathud_marketplace_bans;
DROP TABLE IF EXISTS f5_combathud_marketplace;
DROP TABLE IF EXISTS f5_combathud_admin_audit;
DROP TABLE IF EXISTS f5_combathud_player_crosshair_profiles;
DROP TABLE IF EXISTS f5_combathud_player_crosshairs;
DROP TABLE IF EXISTS f5_combathud_schema_version;

Start the resource afterwards and it rebuilds everything from install.sql. This 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.

Keep reset SQL out of install.sql

When a whole table is missing, auto-install replays every statement in that file, not just the ones for the missing table. A DROP left sitting in it would run first and the CREATE statements behind it would rebuild the tables empty — the console would then report a healthy schema while every row was gone. Put reset statements in a separate file you import by hand.

See Also​