The admin already had an append-only audit log at the application level. We added a second table and a hash for each entry so the database could detect a rewritten row. Every new audit row now commits to the complete event and to the hash immediately before it.
What we built
The chain table stores a sequence, the audit row id, the previous hash, the current entry hash, and the creation time. The hash is SHA-256 over a versioned canonical JSON string. The serializer fixes the key order and writes nulls explicitly, so changing a timestamp, outcome, path, detail, or any other committed field changes the hash.
The writer adds the audit row and its chain row in one transaction, serializes concurrent appends, and uses database permissions that prevent updates, deletes, and truncation. A historical edit therefore breaks the next hash and every hash after it, while a missing chain row fails the count check.
Why verify in the build
The verification command opens one consistent, read-only transaction, walks the chain in sequence order, recomputes every hash, and confirms that the chain count equals the audit log. It reports the first break and exits nonzero. The production build runs the migration, backfill, and verification before the application build, so a release cannot pass while the chain is broken or unchained rows remain.
The backfill was needed because the application had already written audit rows before the chain existed. It adds chain entries in (at, id) order and can be run again. Once the chain was live, both rows were required together, so an audit event cannot leave a gap between the log and the chain.
What broke during the build
The first chain implementation needed a review pass. We added serialized appends and a database uniqueness backstop, made verification use a consistent snapshot, and made the verifier release its transaction on failure. The review also documented the cutover race.
The work was small enough to measure. The initial chain commit changed four files with 244 additions and 7 deletions. The follow-up review added 28 lines and removed 7, and the verifier cleanup added 15 lines and removed 6. The evidence is the implementation commits, the two chain migrations, and the golden serializer tests.