Trust · 4 min read
The hash-chained audit log, in 500 words
When a vendor-security team reviews a SaaS product they'll ask "do you have an audit log?" The answer is almost always yes. The follow-up should be: "can you prove it hasn't been modified?"
Most of the time, the answer is no.
A standard audit log is a database table. Each action writes a row. A malicious admin (or a compromised one) can delete rows or rewrite values. If your tool sends you an exported audit log as a CSV, you are trusting the tool's claim that the CSV matches the database, and the database's claim that no one touched it.
This is fine until it isn't. It stops being fine the moment a regulator, a lawyer, or an insurance adjuster needs to know whether a specific action happened at a specific time.
What a hash-chained log does differently.
Every row carries two extra columns: prev_hash and hash. When we write row N into the activity table for a workspace, we:
1. Read the most recent row for that workspace.
2. Take row_N-1.hash as our prev_hash.
3. Compute row_N.hash = SHA-256(prev_hash || canonical_json(row_N)).
4. Write row N with both columns populated.
The chain is per-workspace. Every row links back to the one before it via the hash.
Why this matters.
If anyone alters row_M retroactively, then row_M.hash no longer matches SHA-256(prev_hash || canonical_json(row_M)). The chain is broken at row M, and every row from M forward fails verification.
To silently cover this up, an attacker would need to rewrite not just row M, but also rewrite every row from M forward with newly-computed hashes AND convince the entire history of cached exports, customer-held attestations, and any OpenTimestamps anchors to match. In practice, this is infeasible.
Verification is a public endpoint.
GET /api/admin/audit-log/verify (authed) walks the chain for the caller's workspace and returns:
``json
{
"ok": true,
"rows": 1247,
"firstBadChainSeq": null,
"verifiedAt": "2026-04-20T18:32:11Z"
}
``
An outside auditor, a lawyer, or a regulator can request this attestation at any time. The response proves the chain head matches its contents. If ok is ever false, the tool itself admits the log has been touched.
Why almost nobody ships this.
Because it's work. Every insert site has to route through a helper that reads the chain head, computes the new hash, and writes with the link. 14 insert sites in GoBrief. Also: chains are per-workspace to limit blast radius, which means more bookkeeping. Also: verification is O(n) for a full walk.
Which is fine for creative-ops audit. Our busiest workspace generates ~50 events a day. A full chain verification takes milliseconds.
What comes next.
Per-chain-head anchoring to OpenTimestamps. Every week, we commit the current head hash of every workspace's chain to a public, witness-backed timestamping service. This adds a second independent witness: not only does the chain self-verify, but anyone can prove that a given chain head existed before a given date without trusting us at all.
This is the level of infrastructure you get in a regulated-industry tool. We ship it in creative ops because the upside is easy (credibility) and the downside of not shipping it is catastrophic (the one time a regulator asks).
Open verification endpoint, open Brief Format, public compliance roadmap at /compliance. If you're doing vendor security review on GoBrief, you have everything you need to recommend it.