Skip to content

Erasure and restore-replay runbook

Covers executing a GDPR erasure (ADR-0014) and re-applying erasures after a backup restore. The erasure model is anonymization-first: value-bearing rows survive with their person linkage irreversibly severed; only pure-identity rows are deleted.

Erasure is performed by executeErasure from @emergent/db (src/erasure/execute.ts), which runs the full plan from buildErasurePlan() in one transaction:

  1. Collect identity bindings (including the active supabase-auth subjects for post-transaction auth deletion), launch-scoped LRS credential ids, NRPS context snapshot ids, and audit targets (every (table_name, record_id) belonging to the subject) — before any mutation detaches them.
  2. Delete via path: submission_artifact (learner-created content) through the attempt join.
  3. Scrub via path: feedback_entry bodies on the subject’s attempts.
  4. Sever: null person_id (and scrub declared payload columns) on retained rows — memberships, attempts, sessions, telemetry, competency evidence, awards, package progress.
  5. Scrub: activity_event, NRPS member/context snapshots, and every change_audit_entry covering collected audit targets.
  6. LRS: delete statements by IFI (actor / object / context-agent / context-group relations), canonical agents, agent documents; revoke launch-scoped credentials. IFI keys are derived automatically (src/erasure/ifi.ts): the platform convention key account:<EMERGENT_XAPI_ACTOR_HOME_PAGE>::<person id> (plus the built-in default home page), every actor recorded on the person’s launch credentials, and any caller-supplied extras for identifiers minted outside those paths.
  7. Root delete: the person row — FK cascades remove profile, bindings, role grants, affiliations, launch sessions, snapshots; FK set-nulls clear actor columns.
  8. Receipt: insert into erasure_receiptsubject_scope_hash is sha256(person uuid); affected_counts records per-table row counts. The receipt contains no personal data.

After the transaction, executeErasure returns two caller duties:

  • storageCleanup: the submission_artifact URIs and LRS attachment/agent-document storage references whose rows were just deleted. The operator (or wrapping endpoint) must purge these from object storage to complete the erasure.
  • authSubjectsToDelete: the Supabase auth user ids that were actively bound to the person (identity_binding, provider supabase-auth), collected before the root delete. The operator (or wrapping endpoint) must delete each via the Supabase admin API (DELETE /admin/users/:id) — otherwise the subject’s email survives in auth.users. Expired bindings are deliberately excluded: their auth user may have since been re-bound to a different person.

Both are deliberately excluded from the receipt because storage paths and auth subjects can embed or resolve to personal identifiers.

Controller/processor note (ADR-0014): in org deployments the org is controller — requests route through it; for independent learners the platform is controller.

Backups are handled “beyond use”: they expire on normal rotation and are never rewritten. The compliance obligation is that restores re-apply erasures:

  1. Restore the backup into the target environment.

  2. Re-apply all migrations newer than the backup.

  3. Replay erasures executed after the backup’s snapshot time:

    SELECT subject_scope_hash, legal_basis
    FROM erasure_receipt
    WHERE executed_at > :backup_snapshot_time;

    For each receipt, find the restored subject by hashing candidate person ids:

    SELECT id FROM person
    WHERE encode(sha256(id::text::bytea), 'hex') = :subject_scope_hash;

    If a person row matches, run executeErasure again for that person id with the receipt’s legal_basis and requestSource = 'restore-replay'. A non-match means the original erasure predates the data in this backup — nothing to do.

  4. Re-run the replay query until no receipts match restored persons.

  5. Record the replay in the restore’s change log (date, backup id, receipts applied).

Because the live erasure_receipt table is part of every backup taken after an erasure, receipts are themselves restored; replay only needs receipts issued between the backup snapshot and the present, plus any receipts whose scope hash still matches a person (defense in depth: replaying an already-applied receipt is a no-op).

No device action is required (ADR-0014): severed rows stop matching person-scoped row filters and drop from learner replicas on next sync; severed rows are no longer owned, so stale client writes are rejected by RLS. A never-reconnecting device is a device-security concern, not an erasure concern.

  • Archival media are never rewritten (beyond-use doctrine; ICO/CNIL).
  • Wallet/verifier copies of exported credentials: revocation/status-list update is the Art. 17(2) “reasonable step” (ADR-0015).
  • Research Projections retain de-identified data under Art. 89 (ADR-0008).