platepilot

Data export and deletion runbook

What exists today

Both actions are per adult, authenticated with the adult’s own bearer token, and audited through AuditService (adult_profile.export, adult_profile.delete). Household membership never authorizes either action for the other adult.

Action Endpoint SDK operation
Export GET /adult-profile/export adultProfileExport
Deletion DELETE /adult-profile with header X-Confirm-Deletion: <adultProfileId> adultProfileDelete

Export contents (format: platepilot.adult-export.v1)

The export is a plain JSON response to the authenticated adult. There is no archive, link, or retention of the export on the server; nothing is written except the audit event.

Deletion sequence (202 Accepted with a receipt)

  1. Authenticate the adult and load their profile.
  2. Require X-Confirm-Deletion to equal the caller’s own adultProfileId. A mismatch is a 400 with details.reason = deletion_confirmation_mismatch and deletes nothing.
  3. Remove retained diary photos from the retention bucket first (deleteRetainedObject for every job with a retained object and no retention-deleted marker). Missing objects are ignored. If the object store is unavailable the request fails with 503 (photo_storage_unavailable) and nothing is deleted, so the adult can retry.
  4. In one database transaction: delete the adult profile (foreign keys cascade to diary meals and food items, water, plan items, client operations, photo uploads, analysis jobs and their outbox rows, privacy preference, sharing settings, sent invitations, subscription, alert preferences), delete the household if it is now empty (the other adult’s household is untouched otherwise), and write a deletion_receipts row holding only a one-way hash of the identity subject.
  5. Return { receiptId, deletedAt, status: "completed" }. The audit event records the receipt ID as its resource. Subsequent requests with the same token receive 404 Adult profile not found until the adult onboards again.

Late worker activity cannot recreate deleted data: a claimed job whose rows are gone fails its lease check and the queue message is dropped. Processing-bucket originals that were still in flight fall back to the one-day bucket lifecycle rule.

Local verification

export PATH=~/.local/share/mise/installs/node/latest/bin:$PATH
A='Authorization: Bearer local.synthetic-token'
curl -s -H "$A" localhost:3001/adult-profile/export | jq '.format, (.diary.meals | length)'
ID=$(curl -s -H "$A" localhost:3001/household | jq -r '.members[] | select(.isMe) | .adultId')
curl -s -i -H "$A" -H "X-Confirm-Deletion: $ID" -X DELETE localhost:3001/adult-profile

Integration coverage: apps/api/src/adult-data/adult-data.repository.test.ts (export scoping, cascade, household cleanup, receipt), unit coverage in adult-data.service.test.ts.

To remove all intentionally disposable local Docker data instead:

docker compose down -v

This permanently deletes the local PostgreSQL and LocalStack volumes. It is not a substitute for a per-adult deletion.

Principles

Response periods, identity-proofing method, legal holds, audit exceptions, and support escalation are PENDING legal/privacy approval.

Still pending for production

  1. Re-verification stronger than the bearer token (for example a fresh identity-provider assertion).
  2. Cooling-off period and cancellation window, if policy requires one.
  3. Vendor deletion requests and evidence once real providers replace the mocks.
  4. Backup restore suppression so a restored snapshot reapplies the deletion (35-day ageing target).
  5. Export delivery as an encrypted, expiring archive rather than an inline response.

Verification checklist