Decommissioning
This page takes a FerroEHR instance out of service for good and removes what it
stored. ferroehr db erase deletes the multimedia blobs and every schema the
migrations created, which takes every EHR, composition, template, party, the
party-to-EHR map, the audit trail, the stored settings and the instance id with
it. The rest of the page lists what the command cannot reach and how you remove
it yourself.
To start over with an empty instance of the same release instead, follow Returning to the original state.
- Before you erase
- Stop every server
- The dry run
- The erase
- What the erase does not reach
- Removing a single party
- How the procedure is tested
Before you erase
Warning
The erase cannot be undone. Nothing in FerroEHR keeps a copy.
You are the controller of what the instance holds, and the law may require you to keep some of it after the instance is gone. Settle these before you run the command:
- Clinical records. Medical-records law sets retention periods measured in
years after the last treatment or after death. If the records must survive
the instance, move them first: export each EHR over the API, or take a full
dump through
{base}/admin/dump(dump and load), and check the copy in its new home before you erase. The retention marks and what is already due are described in Retention, restriction and objection. - The audit trail. Access logs have a retention floor of their own: three
years from each access in every EU Member State, five in the Netherlands, one
in Switzerland (Audit). The erase deletes the
auditschema. Take a dump of it first and keep that dump for the longest period that applies to you (Backup and point-in-time recovery). A dump kept for this reason is still personal data and needs the same protection as the instance had. - Data subjects and processors. If the instance served other organisations or fed other systems, tell them before the data goes.
Stop every server
Stop every FerroEHR server and job that connects to the databases: the replicas, the migration Job, the backup CronJobs and anything else that holds a connection. A server that keeps running holds locks, and the erase gives up after 30 seconds of waiting for one rather than hang. A server that keeps running could also write a new blob after the blobs are deleted.
With the Helm chart, helm uninstall (see Kubernetes
below) stops all of them. With Docker Compose, stop the ferroehr service and
leave the database running.
The dry run
Run the command once without --confirm. It erases nothing, prints what an
erase would delete, prints the value --confirm must carry, and exits with
status 1:
$ ferroehr db erase
`ferroehr db erase --confirm` would permanently delete:
12 multimedia blob(s) in bucket `openehr-multimedia`
database `ferroehr`: schemas audit, linkage, party, clinical, ext
instance id: 0b5e3c1e-6f4a-4d1b-9a43-2b7c5f1d8e90
to erase, run: ferroehr db erase --confirm 0b5e3c1e-6f4a-4d1b-9a43-2b7c5f1d8e90
Error: dry run: nothing was erased
The command reads the same configuration the server does (--config,
FERROEHR__… and --set), so run it with the configuration of the instance
you mean to remove. When the domains live in several databases, it prints one
line per database.
The value to confirm with names the instance:
- The instance id, when the database stores one. The server creates it at the first start with the usage report switched on.
- The name of the database the clinical domain lives in, when the database stores no instance id, because the usage report never ran.
Reading the value off the dry run is deliberate: you confirm the instance the configuration actually reaches, not the one you think it reaches.
The erase
ferroehr db erase --confirm 0b5e3c1e-6f4a-4d1b-9a43-2b7c5f1d8e90
The command runs in this order:
- It lists the blob store. A store that is configured but cannot be reached stops the command here, before any database is touched.
- It checks the confirmation. A value that does not match stops the command with nothing erased.
- It deletes every multimedia blob FerroEHR wrote to the bucket. These are the objects at the bucket root named by a 64-character lowercase hex SHA-256; any other object in the bucket is left alone.
- For each database, in one transaction, it drops the schemas
ext,clinical,party,linkageandauditthat live there, withDROP SCHEMA … CASCADE. The cascade takes every table, row, function, partition and stored setting in them, and thebtree_gistextension installed inext. A database is either erased whole or left as it was.
It connects as ferroehr db migrate does: on [db] migrate_url (or
[db] url when that is unset) for every domain in that database, and on a
relocated domain’s own DSN otherwise. That credential must own the schemas,
which the credential that ran the migrations does.
If a step fails, fix the cause and run the same command again. Blobs are deleted before the schemas, so a failure while deleting blobs leaves the database whole. A second run after a successful one finds nothing, says so, and exits 0.
A build without the multimedia feature refuses to run against a
configuration that names a blob store, because it cannot delete the blobs.
What the erase does not reach
The erase removes what FerroEHR stores in its databases and its bucket. The following are outside them, and removing them is your job.
Backups and dumps
Your pg_dump files, base backups and volume snapshots still hold everything.
With the chart’s backup CronJobs (backup.enabled), the dumps are on the
PersistentVolumeClaims you named in backup.clinical.persistentVolumeClaim,
backup.party.persistentVolumeClaim, backup.linkage.persistentVolumeClaim
and, when the audit domain has its own database,
backup.audit.persistentVolumeClaim. The chart created none of these claims,
so helm uninstall leaves them. Keep the ones your retention duties require
and delete the rest.
WAL archives, replicas and standbys
A WAL archive (pgBackRest, WAL-G, archive_command) holds every change the
database ever made, including the rows the erase dropped. A streaming replica
or standby replays the drop, but a delayed standby, a logical replica or a
replica you detached keeps its copy. Expire the archive and remove every
replica once your retention period allows.
The databases and the roles
The erase drops schemas, not databases. Drop each database when nothing else lives in it:
DROP DATABASE ferroehr WITH (FORCE);
Run it as the owner or a superuser, connected to another database
(DROP DATABASE).
Repeat it for every database [storage.party], [storage.linkage] or
[storage.audit] names.
The roles are cluster-wide, so neither command removes them: the migrations
create ferroehr_migrator, ferroehr_clinical, ferroehr_clinical_reader,
ferroehr_party, ferroehr_party_reader and ferroehr_linkage, and you
created the login roles your DSNs use. Drop them when no other database on the
cluster uses them:
DROP ROLE ferroehr_clinical, ferroehr_clinical_reader, ferroehr_party,
ferroehr_party_reader, ferroehr_linkage, ferroehr_migrator;
DROP ROLE refuses a role that still owns objects or holds privileges in any database of the cluster.
Kubernetes and Helm
helm uninstall removes what the release created, including the Secret the
chart renders from secrets.* values:
helm uninstall ferroehr --namespace ferroehr
It leaves what you created yourself: the Secrets named in the
existingSecret values (the DSNs, the migrator DSN, the backup DSNs) and the
backup claims. List them and delete each one you no longer need:
kubectl get secret,pvc --namespace ferroehr
kubectl delete secret <name> --namespace ferroehr
kubectl delete pvc <name> --namespace ferroehr
Whether a deleted claim’s volume is wiped or kept depends on its storage
class’s reclaim policy
(Persistent Volumes).
A volume with the Retain policy survives the claim and must be deleted on its
own. Delete the namespace last if nothing else runs in it.
Docker Compose
docker compose down --volumes removes the database volume ferroehr-pgdata
and, with the s3 profile, the SeaweedFS volume ferroehr-seaweedfs. The
dumps the backup profile wrote are in ./backups/ next to the compose file.
Configuration, keys and secrets
Delete the files the instance read: ferroehr.toml, the files the *_file
and *_path keys point at (DSNs, TLS keys, the OIDC secret, the signing key
and its passphrase, the national-identifier key, the multimedia secret key),
the licence token, and any .env file. Remove the FERROEHR__… variables from your service definitions
and your secret store. Revoke the credentials themselves where they live: the
database passwords, the object-store access key, the OIDC client and the
broker accounts.
Systems FerroEHR sent data to
Change events on the message broker, records sent to an external Audit Record Repository or a syslog collector, and the logs and traces your telemetry backend received are held by those systems. Remove them there, under the same retention duties.
The physical media
Dropping a schema or deleting an object frees the storage; it does not overwrite it, and a disk, a cloud volume or a bucket can still yield the bytes until they are reused. Sanitising the media (cryptographic erasure of an encrypted volume, the provider’s deletion guarantees, or destroying the disks) is outside what FerroEHR can do. Encrypting the volumes from the start (TLS and database security) is what makes cryptographic erasure possible at the end.
Removing a single party
The openEHR Admin API in ITS-REST defines deletion routes for EHRs only, so
FerroEHR has no REST route that physically deletes a party. The service
implements the specification’s physical_party_delete operation, but no
endpoint exposes it. What you can do without writing code:
DELETEon the demographic API removes a party logically: a new version marks it deleted, and its history stays.ferroehr db eraseremoves every party along with the rest of the instance.
EHRs have physical deletion over REST: Physical deletion.
How the procedure is tested
The integration suite of the ferroehr crate writes an EHR with a composition,
a party, a party-to-EHR mapping, an audit record and an instance id into a
database, checks that a wrong confirmation changes nothing, erases, and checks
that none of the five schemas, and so no row, remains and that a second run is
a no-op. A second test deletes blobs from an in-memory store, and a third shows
that an unreachable store stops the erase before the database is touched. The
command has not been run against a real S3 bucket, a multi-database layout or a
Kubernetes cluster by any probe.