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_idfrom token) - Super Admin / Platform Admin: Can execute erasure on behalf of patient
requested_byaudit field records who triggered it- Super admin override requires
admin:forcepermission - 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:
- Patient already
is_deleted = True→ skip PII wipe step, continue cascade (catches newly-created records) - Hard deletes naturally idempotent (DELETE WHERE returns 0 rows)
- 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)