A tamper-evident audit trail for bookkeeping data
An engineering note from Aha Invoice — for anyone who wants to see how we actually think about a hard problem.
The problem
An audit log that can be edited is a diary, not evidence. Most business software keeps one as a table of rows — and anyone with database access can change a row, delete a row, or insert one after the fact, leaving no trace. For bookkeeping data that's disqualifying: the whole point of the record is that a sceptical third party — an accountant, an inspector, a business partner — can rely on it.
So the requirement we set was narrow and testable: any silent modification of the audit history must be mechanically detectable, including by deletion, including by someone with direct access to the datastore.
The shape of the answer
-
Append-only, per-business sequence
Every mutation that matters writes an audit record carrying a strictly increasing sequence number, assigned atomically per business. A gap in the sequence is itself a finding — deletion doesn't hide a record, it leaves a hole with a number on it.
-
Chaining
Each record binds to its predecessor, so the history forms a chain: alter one record and every record after it stops verifying. Tampering can't be local — to fake one entry you must rewrite the entire tail, consistently, without being noticed.
-
Server-side signing
Records are signed with key material the application tier holds and the datastore never sees. Database access alone is no longer enough to forge history — the attacker now needs a second, separately controlled secret. In production that key lives in a managed key-management service, and signing refuses to run without it; we treat "runs unsigned" as a configuration bug, not a degraded mode.
-
Re-verification as a job, not a promise
A daily integrity job replays the checks — sequence continuity, chain validity, signatures, document hashes — and files anything irregular into an anomaly inbox a human reviews. A guarantee nobody re-checks decays into an assumption; this one is exercised every day.
-
Write-once retention for documents
Issued documents are content-hashed and stored under object-lock retention, so the PDF a customer received can be proven byte-identical years later. The hash lives in the audit chain; the bytes live where even we can't quietly rewrite them.
The details that cost us sleep
- Delivery vs. truth. Audit records are produced through a transactional outbox and drained asynchronously, so a crash between "the mutation happened" and "the record was written" can't lose history — the outbox is part of the same transaction as the mutation.
- Immutability has edges. An issued invoice is frozen, but a business still needs to fix mistakes — so corrections are new documents that reference the original, and closed periods refuse money-affecting changes outright. Deciding which changes are "money-affecting" (a note isn't; a payment is) turned out to be a policy question wearing an engineering costume.
- Recompute from truth. Derived figures shown to users are periodically recomputed from the underlying records rather than trusted forever — the audit trail is only worth something if the numbers on screen agree with it.
What we'd tell another team
None of the individual techniques is novel — hash chains, signing, outboxes and object locks are all well documented. The work is in the composition: making the guarantees hold across crashes, retries, corrections and years of operation, and then checking them every day instead of asserting them once in a design document. That composition is, in our experience, where most audit trails quietly fail.
Aha Invoice is a product we engineer and support. If this is the kind of problem you enjoy, introduce yourself.