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.
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 |
Three doors, all through the one function app.redact_document, which sets app.redacting for the length of its own transaction and nowhere else.
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.public.redact_document(p_document, p_reason), held by the same people who can void (signing.void).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.| 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.
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.
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.
00110, separately.
Free for the first 25 agreements a month. No card to start.