Skip to content

Events & Audit

DPS records activity in several layers, each with a distinct job:

StreamTableRead viaJob
Canonical eventsevents(internal; future consumers)Machine-readable, append-only activity stream — the single answer to "what has been happening in DPS"
Timeline / audit logtimelineGET /audit (rpc_audit_log_v2)Human-readable per-case history shown on dockets and the Audit Log page
Admin auditadmin_audit_logGET /admin/auditForensic System Administration trail: reasons, before/after state, correlation ids
Training eventstraining_eventssession detail payloadsSession-scoped simulation telemetry (idempotent, broadcast)

The canonical stream (events, migration 033) is the newest layer and the reference model; the others are retained for backward compatibility and their specialized jobs.

Event records

Each events row (written only via rpc_write_event):

FieldMeaning
event_typeStable machine-readable type, e.g. report.status_changed
categoryFirst segment of the type; CHECK-constrained to report/contact/punishment/delivery/queue/agent/admin/legacy
actor_idCanonical actor identity — the agent's Discord ID, api-key for the bot system actor, or NULL only when no identity exists
actor_nameDisplay-name snapshot at write time — never the identity carrier
actor_typeagent | system | reporter
target_type / target_idThe resource affected (no FK — audit survives deletion)
report_idDenormalized report link (NULL for non-report events)
metadataStructured detail (reasons, to/from values, refs)
before_state / after_statePre/post images — admin mutations only, mirroring admin_audit_log
correlation_idRequest id tying UI → event → admin audit row
created_atServer timestamp

Actor identity

The identity rules are strict (migration 034):

  • actor_id is the canonical Discord ID, passed as a dedicated parameter (p_actor_id) by every dashboard-driven mutation. The API derives it from the session — never from the client.
  • actor_name is a pure display snapshot. Display names are never the identity; identity is never smuggled into display text.
  • The bot system actor is api-key / BotGhost (API key); reporter actors are the reporter's stored name with actor_type = reporter.
  • Historical backfills assign actor_id only where the stored value is the identity (an exact agents.discord_id/agent_id match); no name-to-ID guessing.

Event taxonomy

The full taxonomy is maintained in event_service.EVENT_TYPES (and mirrored in the reference). Categories: report.*, contact.*, punishment.*, delivery.*, queue.*, agent.*, admin.*, and legacy.timeline_event (historical rows with no machine-readable mapping).

Append-only behavior

events, admin_audit_log, and admin_health_history are append-only:

  • service_role receives SELECT+INSERT only (no UPDATE/DELETE grants).
  • RLS is enabled with zero policies.
  • A BEFORE UPDATE OR DELETE OR TRUNCATE trigger raises EVENTS_APPEND_ONLY (or the audit-log equivalent). The only exception is the guarded 034 actor-identity backfill, which disables the trigger transactionally and re-enables it in all paths — every row written after the migration is fully protected.

Administrative events

Admin mutations (queue maintenance, access management, report correction, agent updates, schema reload) write two records in one transaction:

  1. The forensic admin_audit_log row — agent_id, agent_name, action, target, required reason, before_state, after_state, correlation_id.
  2. The canonical event via admin_write_auditadmin_audit_event_map, linked back with metadata.admin_audit_id.

This means the append-only admin trail and the unified stream can never diverge, and a mutation can never exist without both records.

Mutation events

Report/queue/delivery mutations write their canonical events inside the mutation's transaction (migrations 033/034):

  • rpc_report_actionreport.investigation_started / report.status_changed / report.assigned / contact.reporter_contact_requested / …
  • rpc_add_note / rpc_add_evidencereport.note_added / report.evidence_added
  • queue RPCs → queue.action_claimed / action_completed / action_failed / action_requeued
  • discipline RPCs → delivery.queued / claimed / completed / failed / requeued
  • rpc_delete_reportreport.deleted via the forensic audit dual-write

Route-level event recording (e.g. the report PATCH path) goes through event_service.record_event, which is best-effort by design: a failed event write is logged and never fails the request that produced the activity. The strict path is the transactional one — events required for a mutation's integrity participate in the mutation's transaction.

Audit guarantees

  1. Destructive operations never succeed without their audit record.rpc_delete_report writes the forensic audit (and report.deleted event) in the deletion's transaction; if the audit write fails, the deletion rolls back.
  2. Actor identity comes from an authoritative source. The session's discord_id (verified against the live agents row) or the verified API key; never client-supplied text.
  3. Historical events are not silently rewritten. events is trigger-protected; the only writes are the guarded backfills, which are non-destructive and idempotent.
  4. Audit writes required for a mutation participate in the same transaction. See the RPC list above.
  5. Audit records survive data deletion. timeline and events have no foreign key to reports; a Director's report deletion leaves the audit trail intact (migration 017 retention model).

Why audit records must remain trustworthy

Audit records are the platform's memory: they answer who did what, when, and why, including destructive and administrative actions. If they could be rewritten or silently skipped, an unauthorized action could be erased, a corrupted actor attribution could stand, and the admin audit trail would lose its evidentiary value. The layered protections (grants, RLS, append-only triggers, same-transaction writes, canonical identity) exist so that trust does not depend on any single component behaving correctly.

The timeline (legacy) layer

timeline rows are human-readable ("Report validated — reason…", "Note added …") and remain the backing for the /audit page and docket timelines. Report writers dual-write timeline + canonical event so both stay in sync. rpc_audit_log_v2 resolves by_name from agents (discord_id or agent_id) and scrubs identifiers from all text.

Known limitations

  • training_events is deliberately not mirrored into events: training is a parallel simulation environment with its own event model (client idempotency, realtime broadcast, high volume) and no unified-stream consumer (migration 033 documents this decision).
  • timeline is append-only by convention, not by trigger (noted in AUDIT_LOG_FINDINGS.md); the API never updates/deletes it, and the canonical stream is trigger-protected.
  • events has no public read endpoint yet — it is written for future unified consumers (e.g. a global activity feed or export).
  • Session detail payloads bound training events at 200 rows; long sessions will need pagination/retention eventually.