Where a record came from and what changed since. Core.
audit answers two questions for every stateful row: who or what created it, and what has changed since. An agent writing hundreds of rows from one chat turn is what makes the second question urgent.
Row origin. Every table that holds state carries created-by, updated-by, created-at and updated-at. The actor columns are filled by triggers, never trusted from the caller, so a client cannot post someone else's id and be believed. They are nullable: an ops script or a seed has no person behind it, and null says so. A row a batch wrote also points at that batch — see changes.
Change history. audit_entries is append-only, written solely by a trigger. An insert records the row, an update the changed column pairs, a delete the whole vanished row. Each entry carries the write source of that change: manual, ai, import, api, sync, migration, system. An AI edit to a hand-typed product leaves the insert entry manual and the update entry ai, so one column answers both where a record came from and which fields a model wrote.
Wiring is derived, not listed: a table with the attribution columns gets the triggers. The one derived exclusion is a table signed-in users cannot read, because an entry holds the changed values.
| Route | Content |
|---|---|
/audit | Every entry: who changed what, when, and the change's source. |
| A record's history tab | That record's own entries. |
audit.
audit.read. Nothing writes the history but the trigger. Every read scopes through the same row-level security as the tables beneath.
Never. It is core, cross-cutting, with its own sidebar row and permission.