Skip to main content
Audit in Alfiz is structurally enforced: provenance is validated at the top of every write path, before storage is touched, precisely so no row can land without an attributable entry. This page specifies the properties a compliance review asks about: schema, ordering, retention, immutability, and export.

The record

Log order is (at, then id), so two entries in the same millisecond order by id, identically in every driver, which is what makes cursor paging stable.

Reading and exporting

listAuditEvents takes an AuditQuery with two paging modes:
Without cursor you get the last limit matching entries in log order (the “recent activity” shape). With cursor you get the first limit entries strictly after it, ascending (the export shape). Pass the last entry you received and repeat until a short page.

Retention

Alfiz never prunes the audit log. There is no audit retention setting, no compaction job, and no code path that deletes an audit row. Retention is your database’s policy: keep the table as long as your compliance horizon requires (for SOX that is commonly measured in years), and archive by exporting with the cursor above.
Do not confuse this with the invalidation event log (AlfizEvent), which has documented retention defaults of 7 days / 100 000 rows. That log is cache-freshness plumbing and pruning it affects nothing but cache catch-up. Event-log pruning never touches AlfizAudit, because they are different tables with different jobs.
Hosted audit retention (Alfiz Cloud’s linked tier) is an opt-in copy of this stream for deployments that want a longer horizon than the application database keeps; the local log remains authoritative.

Immutability and the hash chain

The library appends and reads; it never updates or deletes an audit row. But your database will do what anyone with write access tells it to, so the honest posture is append-only by construction, tamper-evident by opt-in:
With the chain on, every entry carries hash, being SHA-256 over the entry’s canonical serialization plus the previous entry’s hash (prevHash). Editing, deleting, or reordering any entry breaks every hash after it:
Three properties to know:
  • It can be enabled mid-life. Existing unhashed entries precede the chain; verification covers the hashed suffix.
  • It is evidence rather than proof. An attacker with database write access and this source code can rewrite and re-hash the whole chain. What makes the evidence hard is anchoring the head hash somewhere they cannot reach: a ticket, a WORM bucket, a signed release note. That anchor is deliberately yours.
  • It serializes appends. Chained writes go through runExclusive("audit", …); multi-node deployments need the driver’s real cross-process lock (the Prisma driver’s options.lock) for the chain to stay linear. The cost is one lock per audited write, paid only by deployments that opt in.
The canonical serialization sorts object keys recursively, so a detail payload hashes identically however a JSON column round-trips key order (Postgres jsonb does not preserve it).

What gets audited

Every write: grants and revokes (create, delete, bulk), roles, groups, memberships, reporting edges, requests (submit, decide, cancel), catalog and import publishes, directory imports (with deprovisioning counts under authoritative sync), virtual-parent dissolution, org-snapshot application, user activation changes, subject and scope deletion sweeps, and reconciliation sweeps. Reads are not audited; permission usage is the metrics feature, not an audit concern. Attribution follows the actor, never the preview: a view-as session records the administrator’s own identity.