The auditor asked what this record said in March
An auditor wants a record as it stood in March and who changed it since. Most systems only promise staff will not edit history. Ours makes the edit impossible.
An auditor is sitting across the desk and asks what a particular record said in March, and who has changed it since. Your system shows what it says today. Somebody in the office says the history is kept, and it is, in a list of changes that everyone has been told not to touch.
The honest answer to the auditor’s next question — could anyone have edited that history — is “we trust our staff not to”. That is a policy, not evidence. A history that anyone with the right login could have tidied up proves nothing, and an auditor who realises that will treat every record accordingly. The cost is not a fine on the day. It is that the whole trail you paid to keep turns out to be worth what a promise is worth.
We ran into this the other way round, on a system we build for field sales teams.
What was actually going on
That system keeps a running history against every sales opportunity: each visit, each note, each change of status, added to the end and never altered. A new piece of code was written to add entries to that history from a phone that might have been offline when the entry was made. Its first draft added the entry and then, a moment later, corrected one detail on it.
Adding and then correcting is two actions, and the second one is an edit. The history’s storage had been set up so that the software’s own login is not allowed to edit or delete anything in it — only to add and to read. So that draft would have failed on the first real day in use, not because the detail was wrong but because the table it was writing to does not accept corrections from anyone.
It was caught before it ran, and only because the ban was already in place. The fix was one line moved: settle the detail first, then add the entry, which is what the attendance check-in code had been doing for weeks. The point is not the near miss. The point is that a rule everybody knew about had been broken by a careful developer within weeks of it being written, and the only thing that stopped it was that the rule was enforced by the database rather than by memory.
The difference between a promise and a lock
In most business systems “the history cannot be changed” is a statement of intent. There is a list of status changes, or a ledger of stock movements, or an audit trail, and everyone knows you are not supposed to alter it. Nothing stops you. The ordinary tools will happily alter it, and so will a developer fixing bad data at three in the morning.
What you actually want is not “we agree not to modify this”. It is “this cannot be modified”. Those are different systems with different failure rates, and the difference costs one line of setup:
REVOKE UPDATE, DELETE ON activity_trail FROM app_role;
The application signs in to the database as a role that can add and read on that table and nothing else. A separate owner role, used only for deliberate, reviewed maintenance, keeps full rights — so a repair that has been argued for and written down is still possible. What is no longer possible is an accidental one, and accidental is the category that actually happens.
Every history table in that system is built this way: order status changes, dispatch status changes, demo status changes, enquiry notes, visit check-ins, stock movements, import rows, the activity trail above. None of them has a “hidden” marker either. Marking a history entry hidden is an edit that makes a row disappear, and a row disappearing from the history is exactly what the history exists to prevent.
Checking that the lock is really on
A ban written into the setup is a claim, and claims about who can do what need checking like any other. So each of those modules carries a small check that asks the database directly which rights the application’s login holds on the table, and insists the answer is “add and read” and nothing more. Not a check that one edit fails in one place — a check on the actual granted rights, which cannot be passed by luck.
This matters more than it sounds, because setup files get edited. The tool that generates those files for this project routinely added changes nobody asked for, and stripping them was a per-change chore. A right quietly reinstated by a tool is invisible to a reviewer and completely visible to that one question.
What it did not cover
The same work exposed the limit of the approach, and it is the part that gets assumed away.
The automatic audit trail works by watching every record that is saved and, if the record belongs to a customer organisation, writing down what changed. That covers essentially every business record, because every business record is tagged with the organisation it belongs to.
Every record except one. The organisation’s own record is not tagged with an organisation — it is the organisation. So changes to its name, default currency, timezone and locale were not captured by the automatic trail at all. The settings record beside it was captured, which made the gap harder to see: the audit history looked populated.
The assumption was “everything is audited”. What had actually been built was “everything with an organisation tag is audited”, and those two sentences describe the same set right up until they do not. Nobody wrote the wrong rule. A mechanism that decides what to do by looking at each record will always have a blind spot exactly where a record is unusual, and the unusual record is normally the important one.
The other gap is anything that is not a change to a record. A spreadsheet export changes no business data. A rewrite of a role’s permissions changes records, but if it is done as a bulk clear-and-reload it goes past the watcher. An attempt to reuse a login token after it has been replaced changes nothing at all — it is a request that should never have happened, and it is the single most interesting thing the trail could record.
So those are written down explicitly, by the part of the system that performs the action, and written through a separate connection that is saved independently of the action itself. That last detail is the point. If the audit entry is saved together with the action, and the action is rejected and undone, the evidence is undone with it. An audit entry for something that was attempted and refused is worth more than one for something that succeeded, and it is precisely the one a shared save destroys.
And the honest limit: none of this prevents someone holding the owner credentials from rewriting history. It was never meant to. It prevents the ordinary path — a helpful refactor, a data fix under pressure, a tool doing what tools do — from doing it by accident, and that is the path every case in this project actually took.
What to ask of your own system
Whoever built your system can answer four questions in an afternoon. Is the history protected by a rule people follow or by a right the software does not have? Is there a check that the right is still withheld, or only a note that it was once? Which record does the automatic trail not see? And is a refused action recorded, or only a successful one?
If the answer to the first is “a rule people follow”, then what you have is a promise, and the auditor is right to treat it as one.
Where this ends up
Sazinga Comply keeps its audit trail on exactly this footing, because the question an auditor asks is not “what does the record say now” but “what did it say in March, and who changed it”. A verification stays visible after the obligation is re-evidenced in a later cycle, and the entries are chained so an edited one does not match its neighbours. If you are reading this because someone asked you to prove a history you are not certain you can prove, the audit log screen is the honest version of what that looks like.