SMART on FHIR

Deleting Data

Note: Opala’s publicly documented APIs are read-only. The guidance and examples on this page describe standard FHIR behavior on a Firely-based server for reference. Write operations and administrative server behaviors are not available on public endpoints.

The FHIR delete operation performs a "logical" delete, meaning the data is not physically removed from the database.

For example, suppose a Patient resource with ID 123 is created (via an HTTP POST /Patient) and subsequently deleted (via an HTTP DELETE Patient/123). This will cause a second version of the Patient/123 resource to be created with version Patient/123/_history/2 that is marked as deleted. This patient will no longer appear in search results, and attempts to read the resource (using an HTTP GET Patient/123) will fail with an HTTP 410 Gone response.

The original content of the resource is not destroyed however. It can still be found using two FHIR operations:

  • Using a FHIR version-specific read: GET https://{baseurl}/Patient/123/_history/1.
  • Using a FHIR instance-history: GET https://{baseurl}/Patient/123/_history.

The HTTP 410 Gone response may include a Location header which contains the fully qualified resource ID as well as the version ID. For example:

410 Gone

Location https://{baseurl}/Patient/123/_history/12

In this example, you can see that the deleted version of the resource is version 12. This means that the last non-deleted version is version 11 and this version can be accessed using a version-specific read to the following URL:

https://{baseurl}/Patient/123/_history/11

Deletes and Referential Integrity

A common problem — frequently in test systems but sometimes in production systems, as well — is cleaning up batches of interdependent data.

For example, suppose you have a FHIR server containing a patient resource with the ID Patient/A. Suppose this FHIR server also contains Encounter/1 and Encounter/2, as well as Observation/3 and MedicationAdministration/4, and all of these resources have a reference to the resource Patient/A. We will call these resources the child resources.

If you try to DELETE Patient/A (using a standard FHIR DELETE operation), this request will be denied, assuming that there is a resource link between the child resources and the patient (a resource link is a field that is indexed with an active SearchParameter).

This will result in an HTTP 409 Conflict with a response like the following:

{
    "resourceType": "OperationOutcome",
    "issue": [
	{
	    "severity": "error",
	    "code": "processing",
	    "diagnostics": "Unable to delete Patient/123 because at least one resource has a reference to this resource. First reference found was resource Observation/456 in path Observation.subject"
	}
    ]
}

If you want to force a delete of Patient/A, you have several options:

  • You can manually delete all of the child resources before trying to delete the Patient.
  • You can disable referential integrity checking in the FHIR Server by changing the Enforce Referential Integrity on Delete setting on the FHIR Storage module. This means that any delete will be permitted as long as the user has appropriate permissions to actually perform the delete, even if other resources still have references left. This can be confusing for other users, since it leaves resources with invalid references, but there are valid cases for doing so.
  • You can use a transactional delete (see below).
  • You can use cascading deletes, if supported by the environment (see below).

Transactional Delete

The FHIR Transaction operation can be used to delete multiple resources at the same time. This is useful if you have chains or collections of resources to delete at once, but also can be used to delete circular references.

To delete multiple resources in a transaction, POST a Bundle such as the following to the root of your FHIR endpoint.

For Example:

POST https://{baseurl}
Content-Type: application/fhir+json

{
	"resourceType": "Bundle",
	"type": "transaction",
	"entry": [
		{ "request": { "method": "DELETE", "url": "Organization/1" } },
		{ "request": { "method": "DELETE", "url": "Organization/2" } }
	]
}  


Note: If any entry fails, the entire transaction is rolled back.

Referential Integrity

By default, Opala will block the deletion of a resource if any other resources have indexed references to the resource being deleted.

For example, suppose the resource Patient/123 has been saved in the repository, and a second resource Observation/456 is then saved as well, where the Observation.subject reference is a reference to the Patient. In this situation, attempts to delete Patient/123 will be blocked unless resources with references to this resource are deleted first (or are deleted as a part of the same transaction in the case of a transactional delete (see above)).

Admin note (reference-only): Some environments allow disabling or scoping referential-integrity checks for specific paths. This is not exposed on Opala’s public APIs; contact Opala if you need this in a managed/private environment.

There are two methods typically used for disabling checking:

  • Globally – where referential integrity checking is disabled globally

  • Selectively – where referential integrity checking is disabled selectively

The value for this setting is a set of one or more FHIRPath expressions, each one on a new line. For example, this setting could be set to Observation.subject in order to allow the deletion described above to proceed.

Cascading Deletes

Certain environments can delete a resource and its dependents in one operation. This feature can be long-running and resource-intensive and is not exposed publicly on Opala’s public APIs. Information in this section is for-reference only and pertains to general administrative activities of a FHIR server.

With cascading deletes enabled, a user can perform the delete on Patient/A (per the example above) and all of the child resources will be deleted as well.

In order to perform a cascading delete, three things must occur:

  1. Cascading deletes must be enabled and supported on the FHIR server.
  2. The user performing the operation must have specific permissions allowing the resource and child resource to be deleted. This typically includes both a general delete permission and a specific cascade delete permission.
  3. To perform a cascaded delete, the client HTTP request must include either a special URL parameter (_cascade) or a special header to indicate that a cascading delete is desired.

The following example shows a delete using a URL parameter:

DELETE https://{baseurl}/Patient/123?_cascade=delete

The following example shows a delete using an HTTP header:

DELETE https://{baseurl}/Patient/123

X-Cascade: delete

Delete Child Resource Count

Depending on the number of child resources linked to a given resource, the cascading delete can potentially take several seconds or even minutes to complete, and can consume considerable memory and processing resources during that time, which may have unexpected impacts on Opala. To minimize the duration and impact of cascading deletes, the actual deletes are performed in batches of a pre-configured size.

If after completing 10 passes of batched deletes, there are still child resources remaining, the deletes will be rolled back and Opala will return an error similar to:

Requested delete operation stopped before all conflicts were handled. May need to increase the configured Maximum Delete Conflict Query Count.

If this type of error occurs after attempting a Cascading Delete request, the batch size can be increased by increasing the configured value of the allowed or expected child resource count deletions.

The $expunge Operation

Important! As noted above, this information applies only to administrative use and environments that support API write access.

Some FHIR servers provide administrative endpoints to permanently remove deleted resources and/or history. These features are dangerous if misused, are not available on Opala’s public APIs, and—where enabled—require strict governance and elevated privileges.

In some cases, you may want to truly delete data; this might be because it was entered in error and should not be seen, because it represents a privacy concern to leave it in place, or because your solution does not require long term retention of stale data.

The $expunge operation can physically delete old versions of resources, deleted resources, or even all data in a database.

A “logical” delete offers a lower risk solution for removing resources; consider expunge only for tightly controlled remediation use cases.

The following categories of the $expunge operation are provided for reference only and are intended for educational, not operational, use. These operations require elevated privileges and careful access controls.

Instance-Level Expunge

Some FHIR servers allow $expunge to be invoked on a single resource instance to permanently remove deleted or prior versions.

Example:

POST https://{baseUrl}/Patient/123/$expunge

Typical parameters may include:

  • expungeDeletedResources – permanently removes logically deleted instances.
  • expungePreviousVersions – removes historical versions of the resource.

Type-Level Expunge

A type-level $expunge targets all resources of a specific types.

Example:

POST https://{baseUrl}/Patient/$expunge

This operation can affect multiple resources and can be used to remove deleted or stale resources of that type as part of administrative cleanup.

System-Level Expunge

A system-level $expunge acts across the entire FHIR repository, removing deleted or historical data across all resource types.

Example:

POST https://{baseUrl}/$expunge

This is the most comprehensive and potentially destructive variant. It is typically reserved strictly for internal remediation or data-purging use cases under system governance controls.

Delete + Expunge

Some FHIR servers combine deletion and expunge in one step, such as:

DELETE https://{baseUrl}/Observation?status=cancelled&_expunge=truee
or
POST https://{baseUrl}/$delete-expunge

These operations perform permanent removal in the background as batch jobs and bypass standard authorization or audit checks.

Batch Execution, Performance, and Governance

Expunge and delete-expunge operations typically run as batch processes on the server.

  • The server deletes resources in batches (for example, groups of 100–1000 resources per transaction).

  • Large jobs may take several minutes to complete and can consume significant memory and database I/O.

  • If too many interdependent child resources remain after a fixed number of passes, the batch may roll back with an error such as:

    Requested delete operation stopped before all conflicts were handled. May need to increase the configured Maximum Delete Conflict Query Count.

They are executed under strict governance policies to prevent accidental data loss, maintain referential integrity, and protect audit trails.

Because of these risks, Opala limits expunge operations to internal, administrative, and managed environments only.