Concept demo

Sample / fictional data throughout. This is a concept demo of a heli-ski / heli-charter operations platform — not a certified or currently-in-use operational safety system, and not affiliated with or representing any specific real operator. Every aircraft, person, and guest shown is invented for demonstration.

CLEARSPAR
← Blog
2026-07-12

The Waiver Signature You Can't Un-Sign

Clearspar Heli is a concept prototype of a day-of-operations system for a heli-ski/heli-charter operation — not a real operator, not real flights, not real guests. Every aircraft and person in the demo is invented sample data, and it is not a certified operational safety system, so no real guest ever signed anything and no real waiver was ever relied on. But the engineering question underneath this feature is real and a little unusual: how do you build a record the application itself is not permitted to change? A signed liability waiver is only worth collecting if, once it lands, nobody — not a bug, not a route someone adds next month, not the person who wrote the app — can quietly edit or backdate it. This post is about making a signature you can't un-sign, and about the one place the same feature deliberately refuses to let anyone override its own rule.

The lazy version of "immutable" is a promise: don't write an UPDATE route, don't write a DELETE route, and call the table append-only. The problem is that a promise like that is only true until the next person — possibly future me — adds a route that breaks it without noticing, and nothing in the system objects. So the guarantee is enforced a layer down, in Postgres itself. On startup the app runs REVOKE UPDATE, DELETE, TRUNCATE ON waiver_signatures FROM its own database role, the same move it already uses to make the audit log tamper-evident. After that, the app's own connection is structurally incapable of altering or removing a landed signature row — Postgres rejects the statement regardless of what the application code asks for. This is not a no-op formality: REVOKE genuinely restricts even the table owner for ordinary DML, and the codebase verified that empirically against a real disposable Postgres before trusting it — as a non-superuser owning role, a REVOKEd UPDATE, DELETE, and TRUNCATE were all rejected with "permission denied," while as a control the identical REVOKE run as the superuser silently succeeded, since a superuser bypasses privilege checks entirely.

TRUNCATE earned its place on that list the hard way, and the story is worth telling because it is this post's own thesis turned back on itself. The first version of that REVOKE listed only UPDATE and DELETE. But TRUNCATE erases every row in a table without issuing a single DELETE, and a table's owner — which is exactly what the app's role is — holds it by default. So for a while the honest description of this system was: the app cannot edit a signature, cannot delete a signature, and can destroy all of them in one statement. Nothing exercised it; no route called TRUNCATE. That is precisely the reassurance this whole design exists to stop depending on. The gap was real on the live database, not a whiteboard hypothetical — a privilege check against production showed the app's role holding TRUNCATE on both the waiver table and the audit log, while UPDATE and DELETE were correctly denied. It was closed by adding one word to the REVOKE, then re-proving it the same way as the original: reproduce the hole (with only UPDATE and DELETE revoked, TRUNCATE succeeded and left zero rows), apply the fix, confirm the rejection, and confirm INSERT and SELECT still work — an append-only table that can't be appended to is a different bug. The protection is exactly as real as the deploy target's role being a non-superuser, and that assumption is checked and warned about at startup rather than assumed.

Immutability is only half of it; the other half is that the record can't be born dishonest. The signing timestamp, signed_at, is set server-side with the database's own now() — there is no signed_at field on the request schema at all, so a client has nothing to send and nothing to backdate. That isn't left to good intentions either: both the public and the client-login sign schemas are declared extra="forbid", so a request that tries to smuggle in an extra field is rejected outright rather than having it quietly ignored. The signer's IP is captured server-side from the real request, not accepted as input. And re-signing is handled with the same care: signing the same document version twice is idempotent and returns the existing row, but signing after the operator has bumped the waiver to a new version inserts a genuinely new row bound to that new version — it never overwrites or reinterprets the earlier one. Public signing runs through a per-guest, single-use tokenized link where only a hash of the token is ever stored and re-minting revokes the prior link, so a leaked or stale URL stops working; the org and guest are resolved from the token itself, never trusted from client input.

Then there's the enforcement that turns a stored signature into an operational gate, and it's the part I most wanted to get right. Assigning a guest to a group runs a waiver check first, and if the guest hasn't signed the current required version, the assignment is refused with a plain error. What makes it interesting is that this block has no override. The app has other compliance gates that a manager can override with a logged reason — an expired guide certification, a failed gear check — and those overrides exist for a good reason: they let a manager vouch for a real-world fact they can independently verify, like a cert that lapsed on paper but that staff genuinely trust is current. A waiver is categorically different. It is the guest's own consent, and a manager clicking "assign anyway, reason logged" cannot manufacture another person's consent without defeating the entire reason you collect a signature in the first place. So this gate is modeled on the non-overridable dispatchability check instead: the honest fix when it blocks is "get the guest to sign," never "wave it through."

The gate is also careful about when it fires, because a compliance block that goes off for the wrong reason trains people to route around it. It only blocks when three things are all true: the org has actually turned on the require-signed-waiver setting, an active waiver document actually exists, and this specific guest hasn't signed that current version. An operator who never opened the waiver config screen blocks nobody — there's no phantom requirement holding up assignments for a feature they never adopted. But once those conditions hold, "unsigned" is a real blocking state with teeth, not a cosmetic warning banner that everyone learns to click past. The message names the guest and the version and tells staff exactly what to do about it.

None of this pretends to be more than it is, and that restraint is deliberate. The seeded starter waiver is titled "Liability Waiver (SAMPLE — not yet configured)" and its body opens by saying, in plain text, that it is a placeholder, does not constitute a real waiver, and is not legally enforceable until an operator replaces it with counsel-reviewed language. The settings screen never claims the e-signature is "legally binding" — the code comments and the guest-facing copy both describe it as a signed consent record with an immutable audit trail (typed full name, server timestamp, captured IP, exact document version), and say outright that legal enforceability is the operator's and their counsel's responsibility, not something the app asserts. The affirmation checkbox is never pre-checked; it defaults to false in the UI and, more importantly, the server-side validator rejects a submission where it isn't affirmed, so an unchecked box can never be silently treated as agreement. Each signature binds to the exact document version the guest actually read, and editing the waiver text later creates a new version rather than reinterpreting what an already-signed guest agreed to.

The lesson generalizes past waivers. When a record has to be trustworthy, the question isn't "did I remember to not write code that corrupts it" — that's a promise you renew, and forget, on every future commit. The question is "what would refuse the corrupting write even if I did." Pushing immutability down to a REVOKE the application can't talk its way past, setting the timestamp where the client can't reach it, and refusing to expose an override for a fact no third party can supply on someone else's behalf are all versions of the same move: encode the guarantee somewhere the next well-meaning change can't quietly undo it. And the honesty half matters just as much — a system that stores a consent record should be exactly as loud about what that record is not as it is about what it is. A signature you can't un-sign is only worth having if you're also willing to say plainly that a signature is all it is.