Research: the table-name config keys
Last updated
May be stale
This page was last updated ago, on 17 September 2026. Check what it says against the code before relying on it.
- Ticket: limelight-development/cityrp2#3435
- Parent map: #3431 — spec for a server-only
cityrp.sessionlibrary atgamemode/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:
- It is client-only.
sh_init.lua:60-66guards the include withif not SERVER then. The server never includes these files — it derivesLLServerfrom the hostname tag and map name atinit.lua:24-44, thenAddCSLuaFiles the single matching file atinit.lua:53purely so the client can learnLLServer.cityrpserveris defined in ansv_file and only ever read server-side. - Ordering. Even if a
core/serverfile did writecityrpserver, on the client it runs fromsh_init.lua, long aftercore/sv_configuration.luawould 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
0001wouldCREATE TABLE IF NOT EXISTS track— creating an empty second table, because theIF NOT EXISTSguard checks the hardcoded name; refund_entswould read and writetrack_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
- Nothing is overridden. The "Overridden anywhere?" column is
notwenty times for the same single reason established in §2 — there is exactly one write tocityrpserverin the repo, and it is the definition. The column is not twenty independent findings. - Two keys are entirely dead:
MySQL RPP Table(rppoints) andMySQL Commendations Table(commendations) have zero readers anywhere in the repo — the stringsrppointsandcommendationsappear nowhere outsidecore/sv_configuration.lua:58-59and 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. - Two keys are not purely cosmetic.
MySQL Membership Table(ll_shared_db.subscriptions) andMySQL Chat Table(ll_shared_db.chat) carry a database qualifier, not just a table name. They point at a different schema from the oneDB_DBNAMEselects, 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 astrack. Neither has a migration, consistent with them living outsidell_cityrp_fw_db. MySQL TableandMySQL Player Tableare duplicates — bothplayers. 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:
- No override exists, has ever existed, or can be introduced through any supported configuration channel (§2, §6).
- Live's own schema export names the tables
trackandonlinelog(§3.2). - 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).
Recommended shape of the change (for #3436)
- Delete
core/sv_configuration.lua:54and:68. - Replace the 12 call sites (§4) with the literal names. In
refund_ents, remove the leading%sfrom each format string and its corresponding argument in the same edit — four of the six sites pass the name as a positionalFRunargument, 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 betterPLUGIN.tableper 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_entsfunctions carry LDoc@ent/@ply/@paramtags (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_entsis a server-only plugin (sv_init.luais its only file). Per AGENTS.md footgun #18, ifcityrp.sessionis ever declared as a.meta.luadependency of anything that loads on both realms, that dependent's client half dies silently.sv_session.luais 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 derivesself.lastSessionwithSELECT 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— ifRunPendingapplies anything, it logs a warning andchangelevels. 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;:44the sole write;:54,:68the two keys;:8-31the DB-name branching)gamemode/env.lua:11-45(ENV:Get,environ)gamemode/init.lua:24-53(LLServerderivation,AddCSLuaFile),:100-127(DB:New(cityrpserver),Migrations.RunPending, migration reboot),:148-155(index ensure using config keys)gamemode/sh_init.lua:50-66(client-onlycore/server/*include, with its own comment)gamemode/core/server/*.lua— all 22 filesgamemode/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, 236gamemode/core/plugins/onlinelog/sv_init.lua:43, 55, 62, 67gamemode/core/plugins/referrals/sv_init.lua:160, 189gamemode/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-liversync steps).gitea/filters/rsync.cd.conf(sync/delete scope)AGENTS.mdfootguns #9, #11, #18, #21- Commits:
c4a635ed7,1df25bd50,e51fa9960,adc429eb3(tracker key history);0ab8efe9f,d4e58a4b7,c8755acb3,576892b99(core/serverhistory)
Sibling research on this map