Message integrity and audit
Nothing here can be quietly changed
A message cannot be edited after it is sent, a removal stays visible as a removal, and the trail of what happened is chained so that taking something out of it can be detected. None of that is a setting somebody remembered to switch on.
- 0
- ways to edit a message once sent
- 0
- roles that can delete a message row
- 2
- layers that independently refuse a deletion
- 1
- removed message still on the record
- Refused by the database, not by the application
- A removal is visible to everyone in the conversation, and names who did it
- Each audit entry covers the one before it, so a gap is detectable
Uneditable
There is no way to edit a sent message, rather than a rule against it
The difference matters more than it sounds. One is a promise about how people behave; the other is a property of the system, and only one of them survives a future feature written in a hurry.
No grant exists
The role the application runs as holds no UPDATE privilege on a message body. A statement that tried to change one is refused by PostgreSQL before any of our code is consulted.
So it is not the application deciding
There is no check to forget, no branch to get wrong, and no admin route that quietly has more privilege than the rest. A developer who wanted to rewrite a message would have to change the schema, in a migration, in a commit somebody reviews.
What that buys you
When a parent, a board or an investigator asks whether a message could have been altered before they saw it, the answer is not “we would know”. It is that the operation does not exist.
Deletion is refused the same way, and twice: the runtime role has no DELETE privilege, and a trigger refuses the statement even for the role that owns the table.
What a removal leaves
A removed message stays where it was, without its words
Every reader of the conversation sees that something was there and is no longer. Four facts remain, and one is deliberately absent.
- Its place in the conversation
- The marker sits exactly where the message sat. A thread that closed a gap would read as though nothing had happened, and the shape of a conversation is part of what a reviewer is reading.
- Who sent it
- Naming the sender is what makes the marker mean anything. “A message was removed” with no author is a hole; this is a record.
- When it was sent, and when it was removed
- Both times, because the distance between them is often the whole story — a message removed within a minute and one removed a week later are different events.
- Who removed it, by name
- The adult who reached into the conversation is named to everybody in it. Where the remover is unknown the marker says so in its own words rather than guessing at a name.
- Not the words
- The body is not shown to anyone in the thread — including the person who wrote it. It is retained for the leaders whose job is to review removals, and it is read on that screen rather than in the conversation.
The plaintext is never even produced for a reader who may not see it. The code declines to decode it, rather than decoding it and then choosing not to display it — a distinction that matters the first time something is logged by accident.
Three decisions, taken deliberately
The parts that were arguments before they were code
Each of these could reasonably have gone the other way. Here is which way they went, and why.
The author sees exactly what everybody else sees
She knows what she wrote; showing it back to her alone would make the removal untrue on the one screen where it most needs to be true. The common case is a young person in distress posting something that should stop being in front of people — herself included.
It names who, and never why
The name is a young person’s only route to challenge an adult who reached into their thread; anonymity would not protect the leader, it would leave the child guessing between the two or three adults who could have done it. The reason is free text an adult wrote for a case file — unscreened, untranslated, able to name a third party — so it lives in the audit trail a reviewer reads, and never on the marker.
A broadcast audience is told too
An announcement is the message most likely to be removed for being wrong, and the people who acted on it are exactly the people who need to know it was withdrawn.
None of these is a default that happened to be left in place. Each was written down, argued, and can be argued again — but not quietly changed.
The audit trail
Every entry covers the one before it
A trail that can be edited is a trail that proves nothing. This one is built so that removing a line breaks every line after it.
Chained, not merely stored
Each entry’s hash covers the previous entry’s hash. Editing or removing any row makes every row after it fail verification, so tampering is detectable rather than merely discouraged.
And refused outright
The database declines UPDATE and DELETE on that table at the privilege layer, and again in a trigger. The chain is the second lock, not the only one.
Written with the change, not after it
Every mutation records its audit entry in the same transaction as the change itself. An action cannot succeed and leave no trail, because the two either commit together or neither does.
That last point is the one that matters most and shows least. An action that succeeded without a record is precisely the hidden activity this product exists to prevent.
At the retention deadline
The words eventually go. Everything about them stays
Evidence is kept for as long as an organization’s jurisdiction requires, and not indefinitely — keeping a child’s words forever is its own harm.
The body empties
Past the deadline, a removed message’s text becomes empty. Not overwritten with a placeholder and not shortened — emptied, and only that.
The record does not
The row, the sender, the position in the conversation, the removal record and the audit chain all survive. What is lost is the words; what happened remains provable.
And one caller can do it
The emptying runs through a single database function granted to the background worker alone. The application role cannot call it, and neither can a person — so it happens on a schedule rather than at somebody’s discretion.
The deadline itself is bounded by the organization’s jurisdiction. A tenant may keep evidence longer than its floor requires; it cannot keep it for less.
In short
What to tell your board
Three sentences, all of them checkable.
- A sent message cannot be edited, because no role in the database holds the privilege to edit one.
- A removal stays visible to everyone in the conversation, names the adult who did it, and keeps the words for the leaders whose job is to review it.
- The audit trail is chained and append-only, and every change writes its entry in the same transaction as the change.