Skip to content

GDPR Article 17 Right to Erasure — Runbook

Related: ADR-0019 (full decision rationale + deletion strategy)

Overview

Curaway processes sensitive patient data (PII + PHI) across PostgreSQL, Cloudflare R2, Upstash Redis, and future Neo4j/Qdrant. GDPR Article 17 ("right to erasure") requires that upon a valid data subject request, all personal data is deleted or anonymized within 30 days.

The deletion handler cascades across 16 data categories following a carefully designed strategy: hard-delete when data has no retention basis, anonymize when compliance proof is needed (audit trail), soft-delete-then-hard-delete when foreign keys exist.

Deletion Strategy by Table/Store

Priority Data Category Table/Store Strategy Retention Basis Notes
1 Chat messages messages Hard delete None (PII text) Immediately after patient requests
2 Conversations conversations Hard delete None (FK to messages) No value without messages
3 Feedback records feedback_records Hard delete None (sensitive corrections) Clinical ground truth — no retention value
4 Match results match_results Hard delete None (reveals medical data) Most sensitive — deleted first
5 Device registrations device_registrations Hard delete None (push tokens are PII) GDPR treats device fingerprints as PII
6 Cases cases Anonymize Audit trail + provider reconciliation Retain id, case_number, status, timestamps; wipe patient_id, procedure_*, ehr_snapshot, etc.
7 FHIR resources fhir_resources Hard delete None (medical data) Delete all Clinical data
8 Documents document_references Mark + queue R2 cleanup Audit trail Retain shell for audit; wipe ocr_text, extracted_data; R2 deletion is async
9 Consent records consent_records Anonymize Compliance proof Retain for regulatory audit (purpose, version, granted_at); wipe ip_address, user_agent
10 Data forwarding audit data_forwarding_audit Anonymize Audit trail Replace patient_id with 'DELETED'; retain counts/dates
11 Consultations consultations Anonymize Audit trail Wipe notes; retain timestamps
12 Notifications notifications Hard delete None (transient) No retention value
13 Events events Anonymize Analytics/audit Replace actor/patient IDs with 'DELETED'; retain event_type for analytics
14 Patient profile patients PII wipe (soft delete) Audit trail via case FK Retain id, tenant_id, created_at; wipe all PII; set is_deleted=true
15 Cloudflare R2 Binary documents Hard delete None Medical images, PDFs, DICOM — no retention basis
16 Redis cache Session state Flush None Best-effort cache invalidation using FLUSHDB or key pattern *{patient_id}*

Facilitator Attribution Cascade (New in D13)

When a facilitator is hard-deleted (after GDPR erasure completion):

Table Cascade Behavior Notes
Patient.referred_by_facilitator_id ON DELETE SET NULL Soft-delete preserves; hard-delete NULLifies
Case.referred_by_facilitator_id ON DELETE SET NULL Soft-delete preserves; hard-delete NULLifies
audit_log entries Append "facilitator_erased" record Historical commissions remain queryable; audit trail captures erasure

Important: Facilitator soft-delete (is_deleted=true) does NOT cascade — it only marks the row inactive. The historical FK references stay intact. Only hard-delete (by GDPR erasure pipeline) fires ON DELETE SET NULL. This is intentional: admins routinely soft-delete facilitators, but historical attribution should remain auditable until the GDPR retention window expires.


Deletion Certificate

After completion, the handler returns a deletion certificate (also stored in audit_log):

{
  "deletion_id": "uuid",
  "patient_id": "patient-xxx",
  "tenant_id": "tenant-xxx",
  "requested_by": "patient",
  "requested_at": "2026-04-13T...",
  "completed_at": "2026-04-13T...",
  "status": "completed",
  "records_affected": {
    "messages_deleted": 142,
    "conversations_deleted": 3,
    "feedback_records_deleted": 5,
    "match_results_deleted": 2,
    "device_registrations_deleted": 1,
    "cases_anonymized": 3,
    "patient_pii_wiped": 1,
    "fhir_resources_deleted": 28,
    "fhir_resources_invalidated": 4,
    "documents_marked_deleted": 12,
    "consent_records_anonymized": 6,
    "forwarding_audits_anonymized": 2,
    "consultations_anonymized": 1,
    "notifications_deleted": 15,
    "r2_files_deleted": 12,
    "redis_keys_flushed": 12,
    "facilitator_fks_nullified": 2
  },
  "r2_keys_failed_cleanup": []
}

API Endpoint

DELETE /api/v1/patients/{patient_id}/data

Request

curl -X DELETE \
  https://api.curaway.ai/api/v1/patients/patient-abc123/data \
  -H "Authorization: Bearer <jwt>" \
  -H "X-Tenant-ID: tenant-xxx"

Response (202 Accepted, then 200 with deletion certificate)

{
  "success": true,
  "data": {
    "deletion_id": "...",
    "status": "completed",
    "records_affected": { ... }
  }
}

Access Control

  • Patient: Can request own erasure (authenticated via Clerk JWT; patient_id from token)
  • Super Admin / Platform Admin: Can execute erasure on behalf of patient
  • requested_by audit field records who triggered it
  • Super admin override requires admin:force permission
  • No other actor type can trigger erasure

Timing

GDPR requires completion within 30 days. Curaway target: immediate (synchronous within the HTTP request). If R2 cleanup fails, the certificate records failures for manual follow-up within the 30-day window.


Idempotency

Running the handler twice for the same patient is safe:

  1. Patient already is_deleted = True → skip PII wipe step, continue cascade (catches newly-created records)
  2. Hard deletes naturally idempotent (DELETE WHERE returns 0 rows)
  3. Anonymize operations idempotent (already 'DELETED' values stay 'DELETED')

External Stores (Future Phases)

Store Status Action When
Neo4j Not current patient data (provider/procedure graph only) Add cascade when patient nodes introduced Phase 0 multi-tenancy
Qdrant Not current patient data (provider embeddings only) Add cascade if patient-specific vectors added If/when needed
Langfuse Contains trace data with patient context API delete or retention policy Phase 3
PostHog Analytics events with patient_id Delete user API call Phase 3

Observability

  • Langfuse: Deletion handler traces via llm_gateway (if LLM used in future)
  • Audit log: Every deletion recorded with certificate + counts
  • Telegram alerts: CRITICAL if deletion fails after COMMIT (data corruption risk)

References

  • ADR-0019: docs/adr/0019-gdpr-erasure-cascade.md — Full decision + strategy rationale
  • Handler: app/services/data_subject_handler.py — Implementation
  • GDPR Articles: 17 (right to erasure), 7(3) (withdrawal of consent)
  • Regulations: GDPR (EU), CCPA (US), DPA 2018 (UK)