Data-subject erasure
Erase or anonymize one person across collections, revisions, activity, comments, notifications, analytics, crash reports, devices and files — with a preview first and a report after.
A deletion request is not a DELETE. One person’s data sits in the end-user
record, the collections that reference them, the revision history of those
rows, the activity log, comments, notifications, analytics, crash reports, their
devices and their uploaded files.
An operator cannot see all of that from one screen — which is why this belongs in backlex rather than in an app on top of it. The relation graph and the physical tables are here.
Two steps, always
# 1. What would this touch? Destroys nothing.curl -X POST /api/admin/erasure/preview \ -d '{"subject":{"type":"email","value":"alice@example.com"},"mode":"anonymize","reference":"DSR-42"}'
# 2. Carry it out. Irreversible.curl -X POST /api/admin/erasure/<id>/run \ -d '{"subject":{"type":"email","value":"alice@example.com"},"confirm":true}'The subject is supplied on both calls, and that is not an oversight — see the record must not re-create what it removes.
Two modes
| Mode | What happens | When |
|---|---|---|
anonymize | Rows survive with identifying fields scrubbed; the end-user record becomes a tombstone (Erased user, …@erased.invalid, is_anonymous) | Usually the only lawful option — an invoice generally cannot be deleted |
delete | Rows and the end-user record are removed outright | When nothing obliges you to keep them |
Revisions are deleted in both modes. Anonymizing a row while keeping its history is theatre: the old address is sitting in the snapshot.
Finding the subject
Two mechanisms, and the split is deliberate:
- Ownership is authoritative. An owner-scoped collection says outright whose row it is.
- Email matching is a heuristic. Any text field declared as an email
(
interface: "email",validation.format: "email", or simply namedemail) is matched against the subject’s address. That finds the common case — acustomerstable keyed by address — with no configuration.
The heuristic can only match when the value equals the subject’s address, so a false positive is a row that genuinely contains their email. The preview exists so you see exactly what was found before anything is destroyed.
Adopted tables are skipped. They belong to somebody else’s application and you never told backlex what their columns mean.
An address with no account is still a subject: it may appear in a collection, and “no user row” is not “nothing to erase”.
What a run reaches
| Surface | Action |
|---|---|
collections | Anonymized or deleted per mode |
revisions | Deleted — always |
files | Stored object deleted first, then the row — deleting only the row leaves the bytes in the bucket with nothing pointing at them. Objects the adapter could not remove are counted as filesUnreachable |
comments | Deleted |
notifications | Deleted |
activity | Deleted, not scrubbed — the row carries IP and user agent beside the id, so nulling the id alone leaves the person identifiable |
analytics | Deleted |
errors | Deleted — a crash report carries a stack and a free-form context blob |
devices | Push tokens deleted |
identity | Sessions, accounts, roles, org memberships, external identities and phone numbers deleted; the user record deleted or tombstoned per mode |
What it cannot reach
Returned on every request as limits, because a tool that ignored them while
reporting completed would say a legal obligation is discharged when it is not:
- Backups taken before the request still contain the subject. That is a retention-policy matter, not something erasure can reach.
- Data already delivered to third parties through integrations, webhooks or a warehouse sync must be erased at those destinations separately.
The record must not re-create what it removes
An audit row reading “we erased alice@example.com” outlives every row it deleted — it is the one place the address would survive the erasure.
So the request row stores no address and no user id. It keeps:
subject_hash— SHA-256 ofAUTH_SECRET + tenant + type + normalized subject. Salted on purpose: an unsalted hash of an email is not a pseudonym, because the space of real addresses is small enough to enumerate.subjectRef— the first 12 hex characters, enough to see that two requests concerned the same person and useless on its own.reference— your own ticket id. Yours to keep free of personal data.- Per-surface counts in
planandreport. Counts, never values.
That is also why the run re-supplies the subject: there is no stored address to act on, and matching the hash is what proves the second call means the same person.
Guards
- A run refuses a request that was never previewed.
- A run refuses a different subject than the one previewed.
- A run refuses to replay a completed request.
confirm: trueis required, so an empty body cannot trigger one.- The run re-locates the subject rather than acting on the stored plan — a preview may be days old, and a stale list would both miss rows written since and try to delete rows already gone. The plan is a preview, not a work order.
- A failed run is left
failedwith the reason rather than rolled back. Erasure is not transactional across these surfaces, so a partial run is a fact the operator has to see and re-run.
Surfaces
| Surface | Entry point |
|---|---|
| REST | /api/admin/erasure (+ /surfaces, /preview, /{id}/run) |
Admin-only, and scoped to the active workspace throughout — another workspace’s request cannot be read or run, and its collection rows are never counted or touched.