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.
The run. A batch that wrote records, an agent turn, a spreadsheet import, a storefront sync, is a row in audit_runs, with its channel, its status, its counts and its origin. Every table a feed or an import can land carries a pointer back to the run, so a batch opens its records as an ordinary grid filter composed with the tenant's own filters and pagination.
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 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.
Generated data lands unapproved. An imported or AI-written row with no explicit status is a draft, invisible on the storefront until a person approves it, and a re-import never reverts a status a person decided. Approval is a guarded transition, not a table; the entries already record who, when and the from-to pair. A tenant can require a second person.
| Route | Content |
|---|---|
/audit | Every entry: who changed what, when, and the change's source. |
/audit/batches | One row per run, linking to the records it produced. |
| A record's history tab | That record's own entries. |
audit.
audit.read. 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.