Append-only audit log
An append-only audit log lets entries be inserted but never altered or deleted, enforced at the database-privilege level, not just app code. Caisson hash-chains each entry to its predecessor in the kernel, then the Compliance bundle's audit-worm package anchors the chain's length and tip hash write-once to WORM storage, so tampering, reordering, or truncation each surface on verify.
In code
export function verifyChain(
entries: readonly AuditChainEntry[],
anchor?: AuditChainAnchor,
): ChainVerification {
for (let i = 0; i < entries.length; i++) {
const entry = entries[i] as AuditChainEntry;
const expectedPrev =
i === 0 ? null : (entries[i - 1] as AuditChainEntry).hash;
if (entry.seq !== i) return { valid: false, brokenAt: i };
if (entry.prevHash !== expectedPrev) return { valid: false, brokenAt: i };
if (entry.hash !== hashChainLink(entry.prevHash, entry.payload)) {
return { valid: false, brokenAt: i };
}
}
// Internal consistency alone can't see a truncated tail or a wholesale rewrite;
// a trusted WORM anchor catches both by asserting the committed length and tip.
if (anchor !== undefined) {
if (entries.length !== anchor.length) {
return { valid: false, brokenAt: Math.min(entries.length, anchor.length) };
}
const tip = entries[entries.length - 1] as AuditChainEntry;
if (tip.hash !== anchor.tipHash) {
return { valid: false, brokenAt: entries.length - 1 };
}
}
return { valid: true, brokenAt: null };
}How it holds
Immutable by privilege, not convention
The audit_chain_entry table's migration grants the app role SELECT + INSERT only, UPDATE and DELETE are never granted, and FORCE ROW LEVEL SECURITY holds even for the table owner. A compromised or buggy query can append a row; it cannot rewrite or drop one.
Each entry hashes over its predecessor
Every entry's hash is SHA-256 over a canonicalized [prevHash, payload] tuple, with payload keys deterministically sorted so the hash is reproducible across machines. Edit, reorder, or drop a middle entry and every hash after that point breaks; verifyChain returns the first broken index.
A WORM anchor catches what the chain alone can't
Internal consistency doesn't prove completeness, a truncated tail or a wholesale-rewritten chain can still verify clean on its own. Every append mints a {length, tipHash, genesisHash} commitment and writes it write-once to WORM object storage, so a length or tip mismatch on read-back proves truncation or rewrite.
No forked chains under concurrent writes
Appends for one tenant serialize under pg_advisory_xact_lock, and UNIQUE(account_id, seq) is the hard belt underneath it: two racing appends that read the same tip collide on 23505 and the loser gets a ConflictError to retry against the new tip.