
Data retention and deletion
Last updated in the repository as docs/security/DATA_RETENTION.md.
What is removed when an agreement is redacted, what stays so the evidence still proves what it proved, and when each happens.
Status: implemented in 00188_retention.sql, asserted by supabase/tests/188_retention_test.sql (32 assertions), evidenced daily as control C1.2 by app.compliance_check('retention_in_force'). The first version of this page said there was nothing here. This one says what there is.
The rule
Redaction, not deletion. The value of an agreement's evidence is that it is still there when somebody disputes the agreement, which may be years after the customer stopped paying us. So what goes is the text and the people, and what stays is everything that proves a named person once signed a text with this fingerprint at this time.
| Goes | Stays |
|---|---|
| The document body (replaced by a notice with the date) | The SHA-256 fingerprint of the body, kept as the column rather than recomputed |
| The signers' names and email addresses (replaced by "Redacted" and an address that cannot receive mail) | That a signer existed, their role, their order, and when they consented and signed |
| Typed, drawn and uploaded signatures on the events | Every event, its kind, its time, its own fingerprint of the text as it stood |
| Network addresses and browsers on the events | The certificate, its verification code, its issue time and both of its hashes |
| The answers to a workflow's questions | The audit line saying who redacted it, when, and why |
| The body, subject and recipient of every email or text about it | That a message was queued, by which channel, and whether it went |
| The document's title (replaced by "Redacted document") | The document's id, status, and every date on it |
The signed copy kept since 00453 — the sealed PDF itself, and its key | Its SHA-256, its length and when it was made, so a copy somebody was sent before still checks out at /verify |
When
Three doors, all through the one function app.redact_document, which sets app.redacting for the length of its own transaction and nowhere else.
- The retention sweep. Hourly, from the worker (
src/worker/sweeps.ts),public.sweep_retention()redacts every finished document past its tenant's period and every delivered message past its tenant's message period. "Finished" is completed, declined, expired or void; a document somebody may still sign is never touched, whatever its age. - The customer. On a finished document's page in the portal, "Redact the people from this document", with a reason. This is how a signer's request to be forgotten is served: the signer is a data subject with rights against the business that sent the document, and that business now has a tool.
- The API.
public.redact_document(p_document, p_reason), held by the same people who can void (signing.void). - Closure (
00250). When the owner closes the business, the sweep treats every finished document as due thirty days after the closing date or at the end of its period, whichever is first, and says which on the redaction reason. Before closing, and for those thirty days after, an owner or admin can take everything the business holds as one file — Settings → *Download the records* — with the whole audit trail in it and no secret.
The periods
| Setting | Default | Floor | Ceiling |
|---|---|---|---|
retention.document_days | 2,555 (seven years) | 365 | 7,300 |
retention.message_days | 30 | 1 | 365 |
Per tenant, in tenant_settings, through the same definitions every other setting uses. The floor on documents is a year because a retention period shorter than any contract's statute of limitations is a period that deletes evidence the customer will need; what the right period *is* where the customer operates is their call, not ours, which is why it is a setting.
The demo account (00191, slug demo) is the one exception in the other direction: public.sweep_demo() redacts a signed demo document a day after it completes, and its unsigned documents expire after a day because the demo workflow's links last a day. Its message bodies go after a day too (retention.message_days = 1 for that tenant). Nothing else is touched.
Our own terms of service, signed in onboarding, are excluded from the sweep: they are the contract between us and the customer and are kept for the life of the account. A customer can still redact that document by hand.
What a certificate says afterwards
A byte-for-byte comparison of the live record with the payload frozen at issue would now say "tampered" about a lawful erasure. So when a document is redacted the same personal details are scrubbed from the frozen payload — the one write signing_certificates_immutable allows, and only of that column — and app.verify_certificate compares the two with those details scrubbed from both sides. Everything that was not redacted must still agree.
The verify page then shows a green "matches what is kept" and a plain notice: *Redacted on [date]. The people's names, addresses and signatures were removed from this record under a retention policy or at a signer's request. The fingerprint of the text, the times and the trail still match this certificate.* A certificate that has genuinely diverged still reads as diverged.
The certificate's own verification_code, document_sha256 and certificate_sha256 never change. The last is now the fingerprint of a payload that no longer exists in full, and the page says so rather than pretending otherwise.
The signed copy (00453)
Since 00453 the sealed PDF is made once, when the document completes, and kept (sealed_copies, encrypted). It carries the text, the names, the signatures and the photographs, so it goes with them: a trigger on signable_documents.redacted_at — the same shape as the photographs' in 00342 — removes the copy's bytes and its key in the redaction's own transaction. The row stays, with its SHA-256, its length and its times, and it cannot be deleted or edited afterwards.
Two guarantees keep that true under concurrency. A copy is stored under a share lock on its document, so a redaction waits for a store already in flight and then withdraws what it stored. And a copy carries the redacted_at its build read; one built from the whole record and arriving after a redaction committed is refused, and rebuilt from the record as it now is.
After redaction the next download builds a copy from the redacted record — what a download produced before 00453 — keeps it once, marked as made from the redacted record, and serves that from then on. The signer's own download link never served a redacted document and still does not (00429). /verify still recognises the original copy by its fingerprint and says the record was redacted, with the date.
What the append-only rules still refuse
signature_events gained a row-level trigger that runs only while redacting and refuses any change to kind, occurred_at, seq, body_sha256, signature_method, detail, or the ids. A redaction that touched a time or a fingerprint is refused like any other edit. Deletes and truncates are refused regardless. The 188 suite asserts each of these by attempting it.
What is not done
- Backups. Redaction does not reach backups until they roll off — the signed copies kept since 00453 included, though they are encrypted there as everywhere under a key the database does not hold. The host's point-in-time recovery window is the real floor on any deletion promise, and any statement we make about deletion has to say so. See BACKUP_AND_DR.md.
- A signer asking us directly. A signer who writes to us rather than to the business that sent the document still has to be pointed at that business. We are the processor; the door above is the controller's.
- Closing an account redacts on a delay, not at once. Thirty days, so the owner can still take the records and the other party to each agreement can still verify it; the certificates and the trail are kept after that, as they are for every document.
- Platform visitors (the marketing site's consent and privacy requests) are covered by
00110, separately.

Ready when the next guest is.
Free for the first 25 agreements a month. No card to start.