# Deletion produces an audit receipt

- **Pattern:** `ab-000787` (`compliance-consent-engine.data-subject-rights.deletion-receipt`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000787
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000787)

## Why it matters

GDPR Article 5(2) requires the controller to demonstrate accountability — when a data protection authority asks "did you process this erasure request?" you need a system-of-record answer, not a search through stdout logs or a best-effort reconstruction from the absence of data. GDPR Article 17 does not just require you to delete data; it implicitly requires you to prove you deleted it. ISO 27001:2022 A.5.28 covers evidence of control operation. A deletion that leaves no receipt is operationally indistinguishable from a deletion that never happened, and log files that get rotated before the DPA inquiry cannot serve as evidence.

## Severity rationale

Low because the erasure itself may be technically complete, but the absence of a durable receipt makes compliance undemonstrable to regulators and forces costly manual reconstruction from backup diffs during any audit.

## Remediation

Create an `erasure_audit_log` table that persists independently of the erased contact row. Return the `receipt_token` to the requester for future reference:

```sql
-- In supabase/migrations/YYYYMMDDHHMMSS_erasure_audit_log.sql
CREATE TABLE erasure_audit_log (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  contact_id      UUID NOT NULL,   -- retain the ID (not PII) for audit linkage
  requested_by    TEXT NOT NULL,
  requested_at    TIMESTAMPTZ NOT NULL,
  processed_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  tables_affected TEXT[] NOT NULL,
  receipt_token   TEXT UNIQUE NOT NULL DEFAULT encode(gen_random_bytes(16), 'hex')
);
```

Return `receipt_token` to whoever triggered the erasure so they can reference the specific event in any future regulatory correspondence. The `contact_id` UUID retained here is not PII — it is a pseudonymous identifier with no PII attached after erasure.

## Detection

- **ID:** `deletion-receipt`
- **Severity:** `low`
- **What to look for:** After a deletion/erasure request is processed, check whether the system produces a durable record confirming that the request was processed. This is important both for internal compliance auditing ("we processed this GDPR request on this date") and for responding to data subjects who ask for confirmation. The receipt should not contain the erased personal data itself — just the fact that the erasure was performed, when, by whom, and which records were affected.
- **Pass criteria:** The erasure service creates a permanent log entry in an erasure audit table (or equivalent persistent store) containing: the request identifier, the timestamp of processing, which tables were affected, and who triggered the request. This receipt is distinct from the erased contact record and survives the deletion. Count all fields on the erasure receipt — at least 4 must be present (contact_id, requested_by, processed_at, tables_affected).
- **Fail criteria:** No erasure receipt is generated. Deletion happens but leaves no evidence it was performed — if the DPA asks "did you process this erasure request?", there is no system-of-record answer.
- **Skip (N/A) when:** GDPR not applicable.
- **Detail on fail:** `"processErasureRequest() performs deletion but writes no audit record — no way to demonstrate compliance for a specific request"` or `"Erasure logged only to stdout — no persistent record survives log rotation"`
- **Remediation:** Write a persistent receipt:

  ```sql
  CREATE TABLE erasure_audit_log (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    contact_id      UUID NOT NULL,  -- retain the ID (not PII), it's needed for the audit trail
    requested_by    TEXT NOT NULL,
    requested_at    TIMESTAMPTZ NOT NULL,
    processed_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    tables_affected TEXT[] NOT NULL,
    receipt_token   TEXT UNIQUE NOT NULL DEFAULT encode(gen_random_bytes(16), 'hex')
  );
  ```

  Return `receipt_token` to the requester so they can reference the specific erasure event.

## External references

- gdpr Art. 5(2)
- gdpr Art. 17
- iso-27001 A.5.28
- nist AU-3

Taxons: regulatory-conformance, observability

HTML version: https://auditbuffet.com/patterns/ab-000787
