---
{
  "title": "$purge: Permanently Erase Patient Data with a Single FHIR Call",
  "description": "Aidbox 2602 implements the FHIR R6 $purge operation — permanently delete a patient and their entire compartment, including all history, in one auditable call.",
  "date": "2026-04-03",
  "author": "Evgeny Mukha",
  "reading-time": "6 min read",
  "tags": ["FHIR Standard", "Aidbox", "Compliance"],
  "tldr": "The FHIR R6 $purge operation permanently deletes a Patient and all compartment resources — including version history. Aidbox 2602 implements it with sync/async modes, custom compartment definitions, and audit logging."
}
---

FHIR's standard `DELETE` is a soft delete. The resource is marked as deleted, but its version history remains intact — every past state is still retrievable via `_history`. For most clinical workflows, this is exactly what you want: an immutable audit trail.

But regulations don't always agree.

GDPR Article 17 grants individuals the "right to erasure." Data retention policies have sunset dates — even if they're 100 years past death. When the time comes to truly remove a patient's data, a soft delete isn't enough. You need the data permanently erased from every version of every resource.

This gap — between FHIR's default soft-delete semantics and regulatory hard-delete requirements — is what the `$purge` operation was designed to close.

## From Zulip discussion to FHIR R6 standardization

The `$purge` operation emerged from years of [community discussion](https://chat.fhir.org/#narrow/stream/179166-implementers/topic/GDPR.20and.20hard.20delete) on FHIR Zulip, starting in 2018.

The central question: how should FHIR handle permanent data erasure? Several names were proposed — `$erase`, `$expunge`, `$gdpr-delete` — and different implementations already existed. HAPI FHIR had `$expunge`, but it only removed previously soft-deleted resources.

The key design decision was scope: should `/Patient/1/$purge` only erase the Patient resource itself, or everything in the patient's compartment? The community converged on the latter — the server should handle the complexity of finding and removing all compartment resources.

The [formal specification work](https://chat.fhir.org/#narrow/stream/179166-implementers/topic/Purge.20and.20Delete.20History.20Operations.20.28FHIR-16198.29) continued in 2023, working through response codes, history deletion semantics, and SMART scopes. Result: `$purge` was added to [FHIR R6](https://build.fhir.org/patient-operation-purge.html) as a normative operation, replacing vendor-specific implementations with a standard.

## Aidbox 2602: $purge in practice

Starting with version 2602, Aidbox implements the FHIR R6 `$purge` operation. A single POST permanently deletes the Patient resource and every resource in their compartment — Observations, Conditions, Encounters, MedicationRequests, and all their historical versions.

The operation uses the [Patient compartment definition](https://build.fhir.org/compartmentdefinition-patient.html) to determine which resources belong to a patient. For each resource type in the compartment, Aidbox queries the corresponding search parameters to find all resources referencing `Patient/<id>`:

```mermaid
flowchart LR
    A["POST /fhir/Patient/pt-1/$purge"] --> B[Load compartment definition]
    B --> C[Delete Patient/pt-1]
    C --> D[Delete Observations]
    C --> E[Delete Conditions]
    C --> F[Delete Encounters]
    C --> G[Delete ...]
```

A basic synchronous purge is a single POST:

```http
POST /fhir/Patient/pt-1/$purge
Content-Type: application/json
```

Response:

```json
{
  "resourceType": "OperationOutcome",
  "issue": [
    {
      "severity": "fatal",
      "code": "informational",
      "diagnostics": "All resources for Patient/pt-1 were purged"
    }
  ]
}
```

After the operation completes, any request for the patient or their resources returns **404 Not Found** — not 410 Gone. The data no longer exists.

## Async mode for large compartments

Some patients accumulate thousands of resources over years of care. Deleting all resources synchronously could lead to timeouts.

Add the `Prefer: respond-async` header to run the purge in the background:

```http
POST /fhir/Patient/pt-1/$purge
Content-Type: application/json
Prefer: respond-async
```

The Patient is deleted immediately. Compartment resources are deleted by background workers. You get back a `202 Accepted` with a `Content-Location` header pointing to the status endpoint:

```http
GET /fhir/$async/<operation-id>     # 202 while in progress, 200 when complete
DELETE /fhir/$async/<operation-id>   # cancel the operation
```

The number of concurrent async workers is controlled by the [`BOX_SCHEDULER_EXECUTORS`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#scheduler-executor-threads) setting (default: 4).

## The shared resource problem: why custom compartments matter

Consider a `Group` resource representing a clinical trial cohort with 50 patients. Or a `List` resource tracking a care team's shared patient panel. These resources reference multiple patients simultaneously. When you purge Patient A, should the entire `Group` be deleted — even though Patients B through Z are still in it?

There is no universally correct answer. Deleting the whole resource breaks referential integrity for other patients who still reference it. Deleting only a portion — say, removing one member from a Group — raises its own questions: should all historical versions of that resource also be rewritten to remove the patient? And does the modified resource even preserve its original semantics?

The FHIR core spec deliberately leaves this behavior to implementers — and for good reason. The right answer depends on your data model, your regulatory context, and how your system uses these shared resources.

`$purge` supports a `compartmentDefinition` parameter that lets you control exactly which resource types are included in the purge. Instead of using the server's default Patient compartment (which includes Group, List, and dozens of other types), you can pass a custom `CompartmentDefinition` that limits the scope.

For example, to purge only clinical data — deliberately excluding Group and List:

```json
{
  "resourceType": "Parameters",
  "parameter": [
    {
      "name": "compartmentDefinition",
      "resource": {
        "resourceType": "CompartmentDefinition",
        "url": "http://example.com/purge-clinical-only",
        "name": "PurgeClinicalOnly",
        "code": "Patient",
        "status": "active",
        "search": true,
        "resource": [
          { "code": "Observation", "param": ["subject"] },
          { "code": "Condition", "param": ["subject"] },
          { "code": "Encounter", "param": ["subject"] },
          { "code": "MedicationRequest", "param": ["subject"] }
        ]
      }
    }
  ]
}
```

This provides explicit control: purge the patient's individual clinical data while leaving shared resources intact for separate handling by your application logic.

The custom `CompartmentDefinition` must have `code` set to `Patient` and at least one resource entry with a non-empty `param` list.

## Built-in safety guarantees

**Idempotent by design.** Purging a patient that doesn't exist (or was already purged) returns success. You can safely re-run `$purge` after a partial failure without worrying about errors.

**Audit trail.** Every successful purge creates an `AuditEvent` with `action: "E"` (Execute) and `subtype: "$purge"`. The purge removes the patient's data, but the fact that a purge happened is permanently recorded. See [how to configure audit log](https://docs.aidbox.app/tutorials/security-access-control-tutorials/how-to-configure-audit-log) for setup instructions.

## Getting started

`$purge` is available in Aidbox starting with version 2602. The endpoint follows the FHIR R6 specification:

```
POST /fhir/Patient/<patient-id>/$purge
```

No additional configuration is required. For complete API details and parameter reference, see the [Aidbox $purge documentation](https://docs.aidbox.app/api/bulk-api/purge).
