Files
recflare/apps/api/migrations/0024_audit_log.sql
2026-09-18 17:55:55 -04:00

41 lines
2.5 KiB
SQL

-- The audit log — one row per privileged action a staff account took. Owned by the `api`
-- worker; WRITTEN by `notify`, which records every call to its admin-gated `/internal/*`
-- endpoints here. Generated from packages/domain/src/audit-db.ts (AUDIT_LOG_SCHEMA_DDL) —
-- keep in sync.
--
-- This is not the worker log. `logger.info` lines are for reading during an incident: they
-- are sampled, they expire, and nothing can be asked of them later. The actions this table
-- exists for leave no other trace on the database at all — a maintenance broadcast is a
-- WebSocket frame and then nothing, and a coach message to every online player is
-- deliberately not stored (see 0019_notification.sql) — so without a row here there is no
-- answer at all to "who sent that, and when", a question that tends to arrive weeks later.
--
-- `player_id` is the ACTOR: the staff account that made the call, never whoever it was
-- aimed at. A target, where there is one, lives inside `data` with the rest of what was
-- asked for. The broadcasts have no target at all, so a column for one would be null on
-- half the table and could not be the thing every row is filed under.
--
-- `data` is JSON as TEXT, and deliberately unstructured: each action records what it was
-- asked to do, and those shapes have nothing in common beyond being worth keeping. It is
-- nullable because an action whose whole content is its name has nothing to put there, and
-- null says that where `{}` would only look like a payload that went missing. Capped at
-- 4096 characters by the writer, since a request body's size is the caller's to choose;
-- over that the row keeps the head of the JSON and says `truncated`.
--
-- `audit_log_id` is a plain rowid alias, not AUTOINCREMENT: the id never leaves the server
-- (unlike `notification`'s, which is handed to the client) and nothing deletes from this
-- table, so there are no freed rowids to be reused. Reads order by it rather than by
-- `date`, which ties when two actions land in the same millisecond.
CREATE TABLE IF NOT EXISTS audit_log (
audit_log_id INTEGER PRIMARY KEY,
player_id INTEGER NOT NULL,
action TEXT NOT NULL,
data TEXT,
date TEXT NOT NULL
);
-- The two ways anyone asks this table anything: everything one staffer did, and every time
-- a given action was taken. Both newest-first, which is the order an audit trail is read in.
CREATE INDEX IF NOT EXISTS idx_audit_log_player ON audit_log (player_id, audit_log_id DESC);
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log (action, audit_log_id DESC);