← All writing
October 2026 7 min read

The database remembers what the application forgets.

A trigger that raises an exception is a stronger guarantee than a comment that says please don't delete this. One cannot be bypassed by a developer in a hurry. The other can.

Most systems that need to retain data for a fixed period enforce that rule the same way: a check in the service layer, a guard in the deletion endpoint, a comment above the function that says, in effect, please be careful here. It works, until it doesn't. A refactor moves the check. A new code path is added that calls the repository directly, skipping the service layer entirely. A developer six months from now, who never read the comment, writes the obvious function and ships it.

The usual version of this rule

This is not a hypothetical failure mode. It is the default outcome of enforcing a rule in application code, because application code has many entry points and the rule has to be remembered at every one of them. Security is engineering, not compliance makes this same argument about authentication and tenant isolation — that a rule a developer has to remember to apply is a rule that will, eventually, not be applied. Retention is the same problem, wearing a regulatory hat instead of a security one.

For VergePass, the rule is specific: lesson records, payments, invoices, and financial documents have to survive for six years, a requirement that comes from BC's driving-school regulations, not from us. Get this wrong and the failure isn't a bug report. It's a compliance violation, discovered by a regulator, after the data is already gone.

THE ACTUAL TRIGGER
ON UPDATE OR DELETE of lesson, payment,
invoice, financial_document
WHERE row is within the 6-year window
  → RAISE EXCEPTION

Added in migration 000065. It runs inside Postgres, not inside the application.

What changes when the database enforces it

Instead of writing the retention rule in the application and hoping every code path respects it, the migration adds the rule to the table itself. Any UPDATE or DELETE on a protected row, from any source, within the retention window, is refused at the database level. Not logged for review. Not flagged for a human to check later. Refused, the same way a foreign key violation is refused — because the constraint isn't a function anyone has to call. It's a property of the data.

This is the same move The schema is the system describes in general terms: the decisions that matter most are the ones made in the data model, because everything built on top of the schema inherits them whether it means to or not. A retention rule written into the schema doesn't need every future engineer to know it exists. It survives engineers who have never read the migration history.

Guarantees: nothing, on its own. It depends on every code path that can mutate the row remembering to call it.

Bypassed by: a new endpoint, a background job, a direct database migration, an admin script, a future refactor.

Fails: silently. The row is gone. No error, because no rule was broken — the one that existed just wasn't consulted.

Guarantees: the row cannot be altered or removed inside the retention window, regardless of what calls the database.

Bypassed by: nothing, short of dropping the trigger itself — a deliberate, auditable schema change, not an oversight.

Fails: loudly. An exception, every time, at the moment someone tries.

Why "impossible" is the right word

The difference between "unlikely" and "impossible" is the whole point. A code review can catch a missing check. It cannot catch a check that was never written, in a code path nobody thought to review, because the author didn't know retention applied there. The database doesn't have that blind spot. It doesn't know what the application intended. It only knows what the schema says is allowed, and the schema says: not this, not yet.

The same principle, one level up

VergePass applies the same instinct to tenant isolation. Which school a request belongs to is resolved from the subdomain — that's routing, nothing more. Whether the request is actually allowed to touch that school's data is a separate chain: the JWT, then a middleware that loads and checks the school, then the handler. A spoofed Host header can point at the wrong subdomain all it wants; it still has to pass through the real authorization chain to touch anything; the two are deliberately kept apart so that getting routing wrong can never, by itself, get authorization wrong.

Both patterns — the retention trigger, the separated tenant check — are the same decision made twice: move the guarantee as close to the data as the system will allow, so that correctness stops depending on every caller remembering to ask for it.

A rule enforced by discipline is a rule waiting to be forgotten. A rule enforced by the database is just true.