The record
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:
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. 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: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:
- 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’soptions.lock) for the chain to stay linear. The cost is one lock per audited write, paid only by deployments that opt in.
detail payload hashes identically however a JSON column round-trips key order (Postgres jsonb does not preserve it).