"We keep an audit log" is true of essentially every platform sold to an MSP. It is also close to meaningless, because the sentence describes a feature and the thing that matters in a dispute is a property.
The property is this: can a person who does not trust you, using a procedure you publish, recompute a number from your data and get the same answer you got? If yes, the record is evidence. If no, it is your word — and your word is exactly what is in question when someone alleges a record was altered.
A hash chain gets you there. Each entry stores a digest of the entry before it, so the entries are linked in one direction and any change to an old entry invalidates every entry after it. That much is widely understood. What is less understood is that a chain has four separate ways of quietly not working, and three of them produce a system that passes every demo.
One — the hash has to be computed over a canonical form
An audit entry is a structured object: who, what, when, which tenant, which object. To hash it you must first turn it into bytes, and the obvious way — serialise the object to JSON — is not deterministic. Two runs can emit the same fields in a different order, and a different byte order is a different digest.
The consequence is subtle and bad. The chain still links. Writes still succeed. But verification fails intermittently on records nobody touched, and the natural reading of an intermittent verification failure is that someone tampered with the data. A chain that cries wolf is worse than no chain, because it destroys the credibility of the one signal you built it to produce.
The fix is to serialise through a stable, key-sorted encoder so the same object always produces the same bytes. It is a small function and it is load-bearing.
Two — the audit write and the business write must be one transaction
If the audit entry is written after the operation it describes, there is a window in which the operation has happened and no record of it exists. A crash inside that window produces a restore, a deletion or a permission change that the trail does not know about. If the audit is written first, the mirror-image window produces a record of something that never happened.
Both are ordinary engineering bugs and both are fatal to the evidentiary claim, because the defect is not "the log is incomplete" — it is "the log and reality disagree, and we cannot say in which direction."
The answer is that the audit row commits in the same database transaction as the write it describes. If the business write rolls back, the audit row rolls back with it. There is no window.
Not "do you have an audit log" — everyone says yes. Ask: is the audit record written inside the same transaction as the operation, and what happens to the record if the operation fails halfway?
Three — concurrent writers can fork the chain
This is the failure that survives the longest, because it needs load to appear. Building the next link requires reading the current last hash and then writing a new row that points at it. Two operations running at the same time can both read the same last hash, and both write a successor to it.
Now two entries claim the same parent. The chain is a fork, not a line. Verification from the tip walks one branch and silently ignores the other, so the branch it does not walk is unprotected — and an attacker who understands the structure has been handed the place to put things.
A single-threaded demo will never show this. A busy Monday morning will. The defence is to serialise chain extension under the strictest isolation the database offers, so that two concurrent writers cannot both read the same parent and commit.
Four — nothing may ever be hard-deleted
A chain entry is not only a record of its own event. It is the anchor for the entry after it. Remove an entry from the middle and its successor now points at a parent that does not exist, and verification of everything downstream fails permanently. Not "fails until you rebuild" — the input that produced those digests is gone.
This collides with ordinary operational instincts. Deleting test data, purging a tenant, cleaning up after a migration: all of them will happily delete audit rows unless the system forbids it. The rule has to be enforced in the code, not in a runbook, because runbooks lose to a 2 a.m. cleanup.
It also interacts with privacy obligations in a way worth thinking about before you are asked. A deletion right applies to personal information; an audit entry recording that an action occurred is not the same object as the data the action touched. Keeping the trail while disposing of the content is a design decision you want to have made deliberately, and to be able to explain.
What this is worth in a dispute
Under FRCP Rule 37(e) the question a court asks is whether a party took reasonable steps to preserve electronically stored information. A verifiable chain does not answer that question by itself — preservation is about scope and process, not hashing.
What it does is change the character of your answer. Without it, "the record was not altered" is a claim supported by your access controls and your good name. With it, it is a claim supported by a procedure the other side can run themselves, over data you hand them, arriving at a digest that either matches or does not.
That is a materially different conversation, and the engineering that separates the two is four properties deep — none of which appear on a feature comparison grid.
Canonical serialisation, one transaction, serialised chain extension, and no hard deletes. Miss the first and verification is flaky. Miss the second and the log disagrees with reality. Miss the third and the chain forks under load. Miss the fourth and one cleanup job destroys verifiability for good.