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 debate to FHIR R6 standard
The $purge operation emerged from years of community discussion 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 continued in 2023, working through response codes, history deletion semantics, and SMART scopes. The result: $purge was added to FHIR R6 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 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>:
A basic synchronous purge is a single POST:
POST /fhir/Patient/pt-1/$purge
Content-Type: application/json
Response:
{
"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 of them synchronously could time out.
Add the Prefer: respond-async header to run the purge in the background:
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:
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 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's 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:
{
"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 gives you 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 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.




