Erasure attestation

Somebody asked to be forgotten, and you deleted them. An auditor, a regulator or the person asks for proof that it happened, correctly. The erasure attestation reads that proof out of your own Worker: the purge’s own record of every step, and the audit events that record each one. It answers for one person or one organization, and for every purge in a period. Nothing is kept by hand, so nothing drifts from what the purge did.

One subject

GET /control-plane/erasure/user/:userId
GET /control-plane/erasure/organization/:organizationId

The answer is one attestation, beside generatedAt:

KeyWhat it says
subjecttype and id. No address, no name: the id is the one value the lifecycle record keeps
deletionWhen it was deleted, by which kind of actor (control-plane, user or system), the reason in their words, the mode (soft or hard), and purgeAfter, when the purge came due. A soft deletion a hard one brought forward reads hard
steps[]One per capability, in the order it first ran: capability, disposition (erased or retained), reason for a retained step or summary for an erased one, and completedAt
statescheduled, in_progress, complete or failed
overdueTrue when the purge is not complete two days after it came due
stoppedAtFor a failed purge, the step it gave up at. For an overdue one, the last step it finished, or due when none has run
completedAtWhen the purge finished, or null until it has
trailrecorded when an audit capability is composed and its events are cited, absent when none is

A kept row is never an erased one. A capability that keeps a person’s rows says why, in a sentence written for the person: payments for tax and accounting records, email so the unsubscribe holds, audit as the record of who did what. The attestation shows that step as retained, with that sentence as its reason. What each capability does with a person’s rows has the list.

A step run again on a later pass is shown once, as it last finished. A purge that failed is started again by the next daily pass, and the steps that finished do not run again.

A subject never deleted, or restored since, is a 404 core/not_found: nothing has been erased. So is a subject kind no composed capability records, such as organization in a Worker without @pithy-sh/organization.

Every purge in a period

GET /control-plane/erasure?since=2026-07-01T00:00:00Z&until=2026-09-30T23:59:59Z

This is the half an auditor actually asks for: every erasure last quarter completed. until defaults to now and since to 90 days before it, and a window is at most 366 days. A window that runs backwards or is longer is validation/invalid_input.

A purge is in the window when it came due in it, ran a step, failed or completed in it, or was still outstanding in it: due before the window opened and neither purged nor restored by then. So a purge stuck since last quarter is named in this one too.

KeyWhat it says
countsscheduled, inProgress, complete and failed, and overdue counted within them
attention[]Every failed or overdue purge, each with the step it stopped at
purges[]Every purge in the window, oldest due first: its subject, state, overdue, purgeAfter, completedAt and stoppedAt
subjectsWhich kinds this Worker records. A kind not listed was not read

The period read counts and names. It cites nothing: ask the subject read for one purge’s citations.

It cites the trail, and signs nothing

Every purge step, the completion and a failure are recorded as audit events: auth/admin_user_purge_step and organization/purge_step with the step and its disposition, …/purge_failed naming where it stopped, and auth/admin_user_purged or organization/purged on completion. The subject read cites the event behind the deletion, each step, the completion and a failure, as { eventId, action, occurredAt, tenant, seq, hash }: where it sits in the chain, never its IP, user-agent or metadata.

Tamper-evidence is the trail’s. The hash chain is what makes those events evident, and pithy audit verify checks the chain the citations point into. Hand an auditor the attestation and an export of that chain. There is no second integrity mechanism, no signature, and no key on the Worker. Without @pithy-sh/audit composed, the attestation still reads the purge’s own record, says trail: "absent", and cites nothing.

A deletion recorded before this release has mode and purgeAfter null, and a step run before it has no citation. The record says what it can.

Reading it

Both routes sit behind their own scope, erasure:read. They are GETs, so pithy dashboard connect preselects it. A connection made before it existed does not hold it, and adds it with --update, naming every scope it already holds beside the new one. No CLI command reads it, and the hosted dashboard does not render it yet, so until it does, read the routes directly with a client of your own. The reference has the shape.

It is its own scope because it answers across subjects: which accounts and organizations were deleted, by which kind of actor, and why. It never widens audit:events:read: it cites an event, never its contents.

It proves, and deletes nothing. Deleting stays where it is, behind auth:users:delete and organization:accounts:delete.

ESC