Security
An audit log you cannot quietly edit
An append-only table is a policy. A hash chain is a property. The difference matters when the person you are proving something to does not trust your database administrator.
Security
Most audit logs are a table with a timestamp. They answer 'what happened' for anyone who already trusts the system. They do not answer 'was this record changed after the fact', because nothing in the data itself would reveal it.
For an agentic platform that matters more than usual. If an agent took an action, the evidence that it did so has to survive scrutiny from someone who has no reason to take your word for it.
Chain each entry to the last
Each row stores the hash of the previous row for the same tenant, plus a hash over its own canonical payload. Altering any historical row changes its hash, which breaks every hash after it.
Detection does not require trusting the operator — it requires recomputing the chain, which anyone with read access can do.
const hash = createHash("sha256")
.update((previous?.hash ?? "") + canonicalise(payload))
.digest("hex");Canonicalisation is the part that bites
`JSON.stringify` serialises keys in insertion order. Two objects with identical content but different construction order produce different strings, and therefore different hashes — so a harmless refactor silently invalidates your entire chain.
Sort the keys. It is four lines, and skipping it produces a system that appears to work until the first verification run months later.
Sequence allocation must serialise
Two concurrent writers reading the same 'latest' row will both chain from it and both claim the same sequence number. `SELECT … FOR UPDATE` on the tenant's last entry serialises them.
This is a real throughput constraint — audit writes for one tenant are serial by construction. For an audit log, that is the correct trade.
Belt and braces
The chain detects tampering. It does not prevent it. Revoke UPDATE and DELETE from the application role and add a trigger that raises on either, so the common case fails loudly and the chain is there for the uncommon one.
