CityRP Documentation Repository

Research: the table-name config keys

Last updated

  • Ticket: limelight-development/cityrp2#3435
  • Parent map: #3431 — spec for a server-only cityrp.session library at gamemode/core/libraries/sv_session.lua.
  • Blocks: #3436
  • Date: 2026-09-02
  • Status: Research complete. Planning only — no gamemode code was changed.

Repo citations are path:line relative to gamemodes/cityrp_2_by_limelightgaming/ unless the path starts docs/, .github/, addons/, lua/ or is AGENTS.md.


TL;DR

GO. Hardcode track and onlinelog. The FK from track.session_id to the new session table stands.

Question Answer
Does any per-server config override either key? No. cityrpserver is assigned exactly once, as a whole-table literal, at core/sv_configuration.lua:44. Nothing in the gamemode, addons/, lua/ or tools/ ever writes into it again.
Can core/server/*.lua override it? No — structurally impossible. All 22 files contain exactly one statement, LLServer = "<name>", and always have (§2.2). They are also client-only includes (sh_init.lua:60-66), and cityrpserver is server-only.
Can env.lua / env-vars override it? No. ENV:Get reads process env-vars by explicit name (env.lua:40-45). Exactly eight names are ever read, and none of them is a table name (§2.3).
Do the live DBs hold non-default names? Cannot be queried from here — stated plainly. But migration 0001 is a literal export of live ll_cityrp_fw_db and creates `track` and `onlinelog` under exactly those names (§3).
Is the indirection already dead? Yes, and worse than dead — it is a latent corruption trap. Migrations hardcode the names while runtime reads the config, so an override would silently split reads and DDL across two tables (§3.2).
Complete call-site list? 12 sites, not 10. Entity Tracker is 6 (ticket said 4); Online Log is 6 (4 + 2, ticket correct). No addons/ sites. (§4)
Any other key in the same position? All 20 are, to varying degrees. Two (MySQL RPP Table, MySQL Commendations Table) have zero readers and are pure dead weight. Two carry a real cross-database qualifier and are not merely cosmetic. (§5)
Confidence, given the out-of-repo caveat? High. The deploy is rsync --delete over gamemodes/*** and lua/***, so an out-of-repo file in the gamemode tree cannot survive a deploy (§6).

1. What the two keys are

core/sv_configuration.lua:44-70 assigns the whole cityrpserver table in one literal:

cityrpserver = {
	["MySQL Host"] = ENV:Get("DB_HOST"),
	...
	["MySQL Online Log Table"] = "onlinelog", -- Table for connection and disconnection logs.
	...
	["MySQL Entity Tracker Table"] = "track",
	...
}
  • core/sv_configuration.lua:54 — ["MySQL Online Log Table"] = "onlinelog"
  • core/sv_configuration.lua:68 — ["MySQL Entity Tracker Table"] = "track"

The table is handed straight to the DB layer at init.lua:108 (DB_MAIN = DB:New(cityrpserver)), which captures the table by reference — so in principle a later mutation would take effect. §2 establishes that no such mutation exists.


2. Is either key overridden anywhere?

2.1 cityrpserver is written exactly once, repo-wide

A repo-wide grep for any assignment into the table:

grep -rn 'cityrpserver' --include=*.lua .

returns one write — core/sv_configuration.lua:44 (cityrpserver = {). Every other hit in the gamemode is a read of the form cityrpserver["…"]. There is no table.Merge, no cityrpserver[k] = v, no setmetatable on it.

Outside the gamemode: grep -rn 'cityrpserver' addons/ lua/ tools/ .github/ returns nothing (the only hits anywhere are two prose mentions in this folder's sibling doc, docs/design/server-sessions/research-fearlessrp-sessions-edge-cases.md:196,244). The vendored addon tree — which is in-repo, so this is a real check and not an assumption — never touches it.

2.2 core/server/*.lua cannot carry config, and never has

There are 22 files in core/server/. Every one is 1–2 lines and contains exactly one statement:

LLServer = "beta";     -- core/server/beta.lua
LLServer = "v4b1_fl2"  -- core/server/v4b1_fl2.lua

Full enumeration: beta, build, event, southside, staging, usv1a, usv1b, usv2, usv2b, usv2d, usv2p, usv33x, v17x, v1a, v1b, v2b, v2d, v2p, v33x, v4b1, v4b1_fl2, v5p. None assigns cityrpserver or anything else.

This is not just true today. Every line ever added to that directory across all of git history:

git log --all -p -- gamemodes/.../gamemode/core/server | grep -E '^\+[^+]' | sort -u

yields 24 unique added lines, all of the form LLServer = "<name>" (the 22 current values, plus a retired usflorida from c8755acb3/576892b99, plus one semicolon variant of v4b1_fl2). One commit — 0ab8efe9f, "feat: added server gm_start with scoreboard and sv_remote vars" — has a message suggesting per-server variables, but its actual diff to core/server/v4b1_fl2.lua adds only LLServer = "v4b1_fl2";. No core/server file has ever contained anything but LLServer.

Two further reasons this directory is the wrong place to look at all:

  1. It is client-only. sh_init.lua:60-66 guards the include with if not SERVER then. The server never includes these files — it derives LLServer from the hostname tag and map name at init.lua:24-44, then AddCSLuaFiles the single matching file at init.lua:53 purely so the client can learn LLServer. cityrpserver is defined in an sv_ file and only ever read server-side.
  2. Ordering. Even if a core/server file did write cityrpserver, on the client it runs from sh_init.lua, long after core/sv_configuration.lua would have replaced the table wholesale on the server. There is no path by which it wins.

2.3 env.lua / environment variables

env.lua is a thin wrapper over the environ binary module: ENV:Get(name) returns environ[name] for an explicitly-named key (env.lua:40-45). There is no bulk import, no prefix scan, no for k, v in pairs(environ). So the set of env-vars that can affect anything is exactly the set of literal names passed to ENV:Get — grepped repo-wide, that is eight:

Env-var Read at Affects
LLServer init.lua:50 server identity
LLServerBetaType core/sv_configuration.lua:27 beta/beta2 split
CITYRP_CHAT_ID core/sv_configuration.lua:41 cross-server chat id
DB_HOST core/sv_configuration.lua:45 connection
DB_USER core/sv_configuration.lua:46 connection
DB_PASS core/sv_configuration.lua:47 connection
DB_DBNAME core/sv_configuration.lua:48 database name
DB_PORT core/sv_configuration.lua:49 connection
GIT_API_URL / GIT_API_BEARER / GIT_WORKFLOW_TO_DISPATCH core/plugins/github/sv_init.lua:25-27 unrelated

No environment variable can rename a table. The deployment-time knob that exists is DB_DBNAME — the database, not the table within it. That is the one axis servers genuinely vary on (core/sv_configuration.lua:8-31 already hardcodes ll_cityrp_fw_db, ..._beta, ..._beta2 by branch), and hardcoding table names does not touch it.

2.4 Git history of the keys

git log --all -S'MySQL Entity Tracker Table' returns four commits, all of which are the original introduction and use of the tracker (c4a635ed7 "Add Entity Tracker Table", 1df25bd50, e51fa9960, adc429eb3). git log --all -S'MySQL Online Log Table' returns thirteen, all in onlinelog/referrals/antihack call sites or doc moves. Neither key has ever been given a non-default value in any commit on any branch.


3. Do the live databases hold non-default names?

3.1 The honest answer

I cannot query the live, beta, beta2 or staging databases from this environment, and I have not. Nothing below is a direct observation of production data. What follows is the code-side evidence and what it implies.

3.2 The code-side evidence is unusually strong here

core/migrations/0001_baseline_live_schema.lua:1-2 describes itself, verbatim:

Baseline migration capturing the live production schema (ll_cityrp_fw_db) as exported 2026-07-28.

That file is not a guess at the schema — it is a dump of Live's actual one, taken six weeks ago. It contains:

  • core/migrations/0001_baseline_live_schema.lua:800 — CREATE TABLE IF NOT EXISTS `track` (
  • core/migrations/0001_baseline_live_schema.lua:559 — CREATE TABLE IF NOT EXISTS `onlinelog` (

So on Live as of the export, the tables were named track and onlinelog — the config defaults, exactly. The track DDL is PRIMARY KEY (server, start_time, ent_index), matching every query in refund_ents/sv_init.lua; the onlinelog DDL carries UNIQUE KEY (steamid, connect, server), matching onlinelog/sv_init.lua:43-67. These are the same tables the code reads.

The migration runner reaches the same DB the gamemode does — init.lua:108-127 runs Migrations.RunPending inside the DB_MAIN:Connect callback, i.e. against cityrpserver["MySQL Database"]. And migrations 0001–0010 are executed on every server: beta, beta2, staging and build all boot through the same GM:ConnectToDatabase. So every environment has had a track and an onlinelog created under those literal names.

3.3 The indirection is not merely dead — it is a trap

This is the finding worth carrying to the map. Every statement in the migration set names tables literally, while every runtime query names them through cityrpserver. The two are already inconsistent by construction. If some server had overridden MySQL Entity Tracker Table to, say, track_v2, then on that server:

  • migration 0001 would CREATE TABLE IF NOT EXISTS track — creating an empty second table, because the IF NOT EXISTS guard checks the hardcoded name;
  • refund_ents would read and write track_v2;
  • any future migration altering track — including the FK this map wants — would apply to the empty one and silently never reach the live data.

The configurability therefore does not currently work end-to-end, and has not since migrations were introduced. Deleting the keys removes a footgun rather than a feature. (Same shape as AGENTS.md footgun #11 — a value duplicated across two places that must agree, with no shared constant forcing it.)

Corroborating detail: two readers already cache the value at file scope into a local — core/plugins/warning/sv_init.lua:18 and core/plugins/caw/sv_init.lua:14 (local MYSQL_TABLE = cityrpserver["MySQL … Table"]). Any late mutation would not even reach them. The codebase already treats these as compile-time constants.


4. Every call site that needs updating

12 sites across 3 files. The ticket's count for the entity tracker was low by two.

MySQL Entity Tracker Table — 6 sites (ticket said 4)

All in core/plugins/refund_ents/sv_init.lua (a server-only plugin — sv_init.lua is its only file):

Line Context
refund_ents/sv_init.lua:94 PLUGIN:Track — INSERT INTO %s (server, start_time, ent_index, …) (in the args table passed to DB_MAIN:FRun)
refund_ents/sv_init.lua:124 ent:CallOnRemove — UPDATE %s SET flagged = 1 WHERE server = … AND ent_index = …
refund_ents/sv_init.lua:154 PLUGIN:Update — UPDATE %s SET data = %s WHERE …
refund_ents/sv_init.lua:167 PLUGIN:Load — SELECT * FROM %s WHERE server = … AND flagged = 0 AND owner_steamid = …
refund_ents/sv_init.lua:219 PLUGIN:Load refund sweep — UPDATE %s SET flagged = 1 WHERE %s
refund_ents/sv_init.lua:236 PLUGIN:OnDBConnected — SELECT MAX(start_time) start_time FROM %s WHERE …

Note lines 94, 154, 167 and 236 pass the name as the first %s of a DB_MAIN:FRun format string, so hardcoding means dropping the argument and the %s together — an easy place to desync the format string from its argument list. Lines 124 and 219 are inline FRun calls.

MySQL Online Log Table — 6 sites (ticket count correct)

Line Context
core/plugins/onlinelog/sv_init.lua:43 INSERT INTO … (steamid, server, ip, token, connect, disconnect, playtime)
core/plugins/onlinelog/sv_init.lua:55 UPDATE … SET disconnect = …, playtime = …
core/plugins/onlinelog/sv_init.lua:62 UPDATE … SET disconnect = …, playtime = …
core/plugins/onlinelog/sv_init.lua:67 UPDATE … SET clientsid = …
core/plugins/referrals/sv_init.lua:160 SELECT steamid, disconnect FROM … ORDER BY disconnect DESC LIMIT 1
core/plugins/referrals/sv_init.lua:189 SELECT steamid, disconnect FROM … ORDER BY disconnect DESC LIMIT 1

Completeness

grep -rl 'Entity Tracker Table\|Online Log Table' --include=*.lua . over the whole repo (including addons/, lua/, tools/) returns exactly four files: the three above plus core/sv_configuration.lua. There are no addons/ call sites and no non-Lua references (no docs, no Python, no JSON). The list is complete.


5. Every other cityrpserver["MySQL … Table"] key

Twenty table-name keys exist. Reader counts exclude the definition in core/sv_configuration.lua. "In migrations" = a CREATE TABLE IF NOT EXISTS for that literal name exists under core/migrations/.

Key Default Readers Reader files In migrations Overridden anywhere?
MySQL Table players 22 sh_premium_token, sh_cppi, sv_debugpremium, sv_player(lib+meta), admin, antihack, referrals, website_rewards, init.lua yes no
MySQL Player Table players 6 discord_relay_free, policedb yes no
MySQL Membership Table ll_shared_db.subscriptions 5 sh_premium_token, sv_debugpremium, sv_membership, discord_relay_free, login_rewards no (cross-DB) no
MySQL Access Log Table accesslog 7 sv_access, antihack, init.lua yes no
MySQL Online Log Table onlinelog 6 onlinelog, referrals yes no
MySQL Log Table logs 2 sv_logs yes no
MySQL Remote Table remote 4 sv_debugpremium, sv_remote yes no
MySQL Chat Table ll_shared_db.chat 8 admin, discord_relay_free no (cross-DB) no
MySQL RPP Table rppoints 0 — yes no
MySQL Commendations Table commendations 0 — yes no
MySQL REP Table reps 4 rep, init.lua yes no
MySQL Warnings Table warnings 1 warning (file-scope local) yes no
MySQL Caw Metrics Table caw_metrics 1 caw (file-scope local) yes no
MySQL Settings Table settings 1 db.lua:72 yes no
MySQL Referral Table referrals 17 referrals yes no
MySQL Referral Tokens referral_tokens 4 referrals yes no
MySQL Properties Table properties 1 doors yes no
MySQL Warrants Table warrants 2 policedb yes no
MySQL Entity Tracker Table track 6 refund_ents yes no
MySQL Inventory Table inventory 4 sv_inventory, init.lua yes no

What that shows

  1. Nothing is overridden. The "Overridden anywhere?" column is no twenty times for the same single reason established in §2 — there is exactly one write to cityrpserver in the repo, and it is the definition. The column is not twenty independent findings.
  2. Two keys are entirely dead: MySQL RPP Table (rppoints) and MySQL Commendations Table (commendations) have zero readers anywhere in the repo — the strings rppoints and commendations appear nowhere outside core/sv_configuration.lua:58-59 and the migration DDL. These are safe to delete outright and are the cleanest candidates for the map's "Not-yet-specified" note. Caveat, and it matters: the tables still exist and may be read by another Limelight service (the website, the forums). Deleting the config key is safe; nothing here licenses dropping the table.
  3. Two keys are not purely cosmetic. MySQL Membership Table (ll_shared_db.subscriptions) and MySQL Chat Table (ll_shared_db.chat) carry a database qualifier, not just a table name. They point at a different schema from the one DB_DBNAME selects, which is genuine information the call sites do not otherwise have. Those two should be hardcoded as shared-DB constants if they are touched at all, not treated as the same trivial case as track. Neither has a migration, consistent with them living outside ll_cityrp_fw_db.
  4. MySQL Table and MySQL Player Table are duplicates — both players. The config's own comment says -- as below left for compatability (core/sv_configuration.lua:50). 28 combined readers split arbitrarily between two names for the same table. Worth a separate cleanup ticket; out of scope here.

6. Can cityrpserver be written from outside this repo?

This is the question the earlier ticket on this map got wrong in the opposite direction — declaring a DB key unused because grep found no readers in this repo, when the reader lived in another service. So the symmetric question deserves a real answer rather than a shrug.

The vectors, and why each is closed:

Vector Verdict
core/server/<LLServer>.lua Closed. Client-only include (sh_init.lua:60-66); one LLServer statement per file, in the whole of history (§2.2).
Environment variables Closed. ENV:Get reads by explicit literal name; the eight names read are enumerated in §2.3 and none is a table name. An operator can set TABLE_TRACK=foo in the Pterodactyl panel all day and nothing reads it.
A DB-backed config Closed. The only settings-table read is db.lua:72, which writes last_start_<server> into settings. Nothing reads config values out of the DB into cityrpserver — and it could not: cityrpserver must be fully populated before DB:New(cityrpserver) at init.lua:108 in order to connect at all. The table name config is consumed before any DB is reachable.
An FTP-dropped lua/autorun file Closed by ordering. lua/autorun/* runs before the gamemode (AGENTS.md footgun #9 states this for cityrp and it applies identically here), so any pre-set cityrpserver is destroyed by the whole-table assignment at core/sv_configuration.lua:44.
An FTP-dropped file inside the gamemode tree Closed by deployment. .gitea/workflows/cd.master.yml deploys with rsync -avzrc --delete --filter="merge .gitea/filters/rsync.cd.conf", and the filter (.gitea/filters/rsync.cd.conf) includes + gamemodes/*** and + lua/***. --delete means those trees are made to match the repo on every push to master, so a hand-dropped .lua under gamemodes/ or lua/ is removed on the next deploy. A persistent out-of-repo override in the gamemode tree cannot survive.
A vendored addon Closed by inspection. addons/ is in this repo; grep -rn 'cityrpserver' addons/ is empty.

The one residual gap, stated honestly: the rsync filter's trailing - * means top-level directories it does not name (cfg/, data/, addons/_*) are neither synced nor deleted. A Workshop-mounted or addons/_-prefixed addon shipping a hook.Add that mutates cityrpserver after sv_configuration.lua runs but before the first tracker query would technically take effect. No such addon is known, no such pattern exists anywhere in the vendored tree, and it would be an extraordinarily strange thing for one to do to a gamemode-private global. This is the residual risk, and it is small enough that I would not hold the FK for it.

Confidence: high. This is a materially stronger position than the earlier ticket's, and the difference is worth naming. That ticket asked "is anything reading this value?" — an open-world question a repo-scoped grep genuinely cannot answer, because readers can live in any service that touches the same database. This ticket asks "is anything writing this value?", and the write side is closed-world: cityrpserver is a Lua global inside one gamemode's server-side Lua state, it is consumed before the DB connection exists, and the deployment guarantees the gamemode tree is byte-identical to the repo. Absence of an override in the repo really is close to absence of an override in deployment here, in a way absence of a reader was not.


7. Verdict

GO — hardcode both table names.

Restating it plainly, per the ticket: this is not a no-go, and the FK decision from #3431 does not need to be reopened. The migration may statically declare FOREIGN KEY (session_id) REFERENCES <session table> (id) on track.

Supporting the go:

  1. No override exists, has ever existed, or can be introduced through any supported configuration channel (§2, §6).
  2. Live's own schema export names the tables track and onlinelog (§3.2).
  3. The indirection is already broken end-to-end — migrations hardcode what runtime configures — so removing it fixes a latent inconsistency rather than removing a capability (§3.3).
  • Delete core/sv_configuration.lua:54 and :68.
  • Replace the 12 call sites (§4) with the literal names. In refund_ents, remove the leading %s from each format string and its corresponding argument in the same edit — four of the six sites pass the name as a positional FRun argument, and dropping one without the other silently shifts every subsequent argument.
  • Prefer a single named constant per plugin (local TRACK_TABLE = "track" at file scope, or better PLUGIN.table per AGENTS.md footgun #21) over 6 repeated string literals, so the FK'd name still has one place to change.
  • Run both diff gates (lint-diff.py, standards-diff.py) — refund_ents functions carry LDoc @ent/@ply/@param tags (refund_ents/sv_init.lua:69-71, 133-134, 163) that standards-diff will want converted to LuaCATS if those functions are touched.

Worth raising separately

# Item Why
1 Delete MySQL RPP Table and MySQL Commendations Table Zero readers repo-wide (§5). Config-key deletion only — say explicitly in the ticket that the underlying tables stay, since another Limelight service may read them. Small, and it belongs with #3436's cleanup rather than after it.
2 Collapse MySQL Table / MySQL Player Table Same value, 28 readers split arbitrarily, comment already admits it is compatibility cruft (§5.4). Bigger blast radius; own ticket.
3 Migrations hardcode table names while runtime configures them The general form of §3.3 — it applies to all 18 remaining keys, not just the two being deleted. Either finish the job (hardcode everything) or make the migration runner read cityrpserver. Leaving it half-and-half is how a future rename corrupts data silently. Candidate AGENTS.md footgun.
4 ll_shared_db. qualified keys are a different animal MySQL Membership Table / MySQL Chat Table encode a cross-database qualifier (§5.3). Any future sweep must not treat them as trivially hardcodable table names.

Fog notes for the map (#3431)

  • refund_ents is a server-only plugin (sv_init.lua is its only file). Per AGENTS.md footgun #18, if cityrp.session is ever declared as a .meta.lua dependency of anything that loads on both realms, that dependent's client half dies silently. sv_session.lua is a library rather than a plugin, so this is a note, not a blocker.
  • refund_ents:OnDBConnected (refund_ents/sv_init.lua:233-246) already derives self.lastSession with SELECT MAX(start_time) … WHERE start_time < <current> — a hand-rolled previous-session lookup that the new session table makes redundant. Worth folding into #3436's scope rather than leaving two session notions in one file.
  • Migrations can trigger a reboot. init.lua:113-121 — if RunPending applies anything, it logs a warning and changelevels. The FK migration will therefore cause one extra restart on the first boot after deploy on every server. Expected, but worth stating in the deploy note.

Sources

Repo (relative to gamemodes/cityrp_2_by_limelightgaming/ unless noted)

  • gamemode/core/sv_configuration.lua:1-70 (definition; :44 the sole write; :54, :68 the two keys; :8-31 the DB-name branching)
  • gamemode/env.lua:11-45 (ENV:Get, environ)
  • gamemode/init.lua:24-53 (LLServer derivation, AddCSLuaFile), :100-127 (DB:New(cityrpserver), Migrations.RunPending, migration reboot), :148-155 (index ensure using config keys)
  • gamemode/sh_init.lua:50-66 (client-only core/server/* include, with its own comment)
  • gamemode/core/server/*.lua — all 22 files
  • gamemode/core/migrations/0001_baseline_live_schema.lua:1-16 (live-export provenance), :559-575 (onlinelog), :800-810 (track)
  • gamemode/core/plugins/refund_ents/sv_init.lua:94, 124, 154, 167, 219, 236
  • gamemode/core/plugins/onlinelog/sv_init.lua:43, 55, 62, 67
  • gamemode/core/plugins/referrals/sv_init.lua:160, 189
  • gamemode/core/plugins/warning/sv_init.lua:18, gamemode/core/plugins/caw/sv_init.lua:14 (file-scope caching)
  • gamemode/db.lua:72 (MySQL Settings Table)
  • .gitea/workflows/cd.master.yml (deploy-live rsync steps)
  • .gitea/filters/rsync.cd.conf (sync/delete scope)
  • AGENTS.md footguns #9, #11, #18, #21
  • Commits: c4a635ed7, 1df25bd50, e51fa9960, adc429eb3 (tracker key history); 0ab8efe9f, d4e58a4b7, c8755acb3, 576892b99 (core/server history)

Sibling research on this map

  • docs/design/server-sessions/research-gm-initialize-boot-predicate.md (#3432)
  • docs/design/server-sessions/research-fearlessrp-sessions-edge-cases.md (#3433)