identity_audit_events, a table with three properties most
application tables do not have.
Append-only, at the database level
A trigger rejectsUPDATE unconditionally, and rejects DELETE of any row
whose organization still exists — the one lawful exception is the cascade
that runs when the organization itself is deleted (see
Delete an organization below), which the
trigger recognizes because the parent row is already gone by the time it
fires. This is not enforced by the API or the service layer — those never
expose an edit or delete path either — it is enforced by PostgreSQL itself,
so a bug, a support script or an operator with a psql prompt cannot quietly
rewrite or remove a live tenant’s history.
Hash-chained, so a rewrite is detectable
Each row carrieschain_seq (a per-tenant sequence) and entry_hash, a
SHA-256 digest of the row’s own fields and the previous row’s hash,
computed by the same trigger that enforces append-only. Deleting or editing a
row at the database level — the one thing the trigger cannot see, because it
only fires on INSERT — breaks the chain: every row after the gap no longer
hashes forward correctly, and verification reports exactly where.
ok: false, the sequence number where it broke, and why. The
one lawful way rows leave the chain is organization deletion, which leaves a
tombstone recording how many rows were erased and the chain head at the
moment of erasure — an operator (or an auditor) can tell “erased because the
tenant closed its account” apart from “someone made rows disappear.”
The chain is computed and verified inside the database with an unkeyed
digest: it detects accidental and most unprivileged tampering, but a database
owner able to disable the trigger and recompute every hash could produce an
internally-consistent forged chain. Anchoring chain heads outside the database
(a signed, periodically published checkpoint) would close that gap and is not
built — do not describe the chain as tamper-proof against a database owner,
only tamper-evident against everything short of one.
What every row carries
Action, resource, and actor — attributed three ways at once when it applies: the human who delegated (actor_user_id), the non-human principal
that acted (actor_workload_id), and the process name, so “a workload acted
under this person’s authority” is one row, not two that have to be joined by
guessing. And where it came from: the client address (as
TRUSTED_PROXIES resolves it — see Deployment),
the User-Agent, and the request ID that joins the row to the access log.
Retention
Audit rows are never deleted by the retention job, at any age, regardless of plan.finding history and usage metering expire on a plan’s window;
identity_audit_events does not, because the schema’s append-only trigger
would refuse the delete in any case, and because a compliance evidence trail
that quietly aged out would be a second, worse surprise. There is currently
no separate configurable retention window for the audit trail beyond “kept
for the life of the tenant” — if your framework requires a documented,
shorter retention with destruction, that is a data-handling policy to state
in your own documentation, not a knob TRUSTIVAN exposes today.
Getting it out: pull, or subscribe
Pull.GET /api/v1/audit/export?format=jsonl|csv streams the trail,
filtered by since/until, with the chain verified fail-closed before
the first byte is written: if verification cannot complete, the export is
refused rather than shipped with an unverified chain header. Requires
identity:read and the audit_export entitlement. See
API reference.
Subscribe. audit.recorded is a webhook event — see
Webhooks — that fires once per appended row, for a
SIEM or log pipeline that wants the trail pushed rather than polled. It uses
the same entitlement as the pull export, and it is deliberately narrower:
no actor identity (only a kind — user, api_key, workload, system,
scim), and none of the row’s free-form detail/before/after fields,
which have carried policy bodies and credential names. What travels is enough
to correlate “what happened, from where, and does it match this access-log
line” — a receiver that needs the acting identity for one event pulls that
row from the export using the delivery’s chain_seq.
See also
- Compliance evidence — the audit trail is one of the capabilities several framework controls cite as evidence.
- Webhooks
- Delete an organization — what happens to the trail when a tenant is erased.

