Backup & Restore
The platform offers two different things, and only one of them is a backup.
| In-app export | scripts/backup.sh | |
|---|---|---|
| Purpose | Move one organisation between installations | Disaster recovery |
| Covers | 12 tenant tables | Whole database + evidence + credentials |
| Excludes | 34 org-scoped tables, incl. evidence and audit log | See what it does not cover |
| Restore | Scoped upsert, users/orgs never deleted | scripts/upgrade.sh --rollback <TS> |
Tenant export (organisation-to-organisation migration)
Section titled “Tenant export (organisation-to-organisation migration)”The in-app Data Management feature exports one organisation’s working data as JSON.
Where it lives
Section titled “Where it lives”Go to Settings, then the Backups section in the settings sidebar. The card is titled Tenant Export / Import, and it says the same thing this page does: a partial export for tenant migration, not a disaster-recovery backup.
Exporting
Section titled “Exporting”- Open Settings, Backups
- Click Download Tenant Export
- A JSON file downloads automatically, named
scf-backup-YYYY-MM-DDTHH-MM-SS.json - Store this file securely — it contains your organisation’s working data in the clear
The export carries:
- All scoped controls and their status
- Evidence tracking configurations
- Organisations and users
- Assignments and comments
- Tasks and notifications
- Metadata (timestamp, record counts)
Importing
Section titled “Importing”- Open Settings, Backups
- Click Import Tenant Data and select your export JSON
- Review the Restore Preview, which is computed against your live data before anything is
written:
- Export creation date and creator
- Which organisations would be written to
- How many existing records would be removed
- Total records and per-table counts
- Click Confirm Restore to proceed, or Cancel to back out
Backup File Format
Section titled “Backup File Format”The platform uses a JSON backup format for portability:
{ "metadata": { "version": "1.1", "created_at": "2025-01-15T10:30:00.000Z", "created_by": "admin@example.com", "table_counts": { "organizations": 1, "scoped_controls": 150, "evidence_tracking": 200, "users": 5, "assignments": 25, "comments": 50 } }, "data": { "organizations": [...], "scoped_controls": [...], "evidence_tracking": [...], ... }}| Field | Description |
|---|---|
metadata.version | Backup format version (currently 1.1; 1.0 files still restore) |
metadata.created_at | ISO timestamp of backup creation |
metadata.created_by | User who created the backup |
metadata.table_counts | Record count per table |
data | Full data for each table |
What Gets Backed Up
Section titled “What Gets Backed Up”| Data Type | Included | Notes |
|---|---|---|
| Organisations | Yes | All organisation records |
| Scoped Controls | Yes | Control selections and status |
| Evidence Tracking | Yes | Evidence configurations |
| Users | Yes | User accounts and roles |
| Organisation Members | Yes | User-org relationships |
| Assignments | Yes | Control/evidence assignments |
| Comments | Yes | All comments and discussions |
| Evidence Tasks | Yes | Task records |
| Notifications | Yes | Notification history |
Not Included
Section titled “Not Included”- SCF catalog data (controls, evidence requirements) — reference data, seeded into the database from the catalogue JSON on the host. It is not shipped with the platform on a self-hosted install and no backup captures it — see the caution below
- System configuration — Managed by the platform
- The credential directory (
SCF_SECRETS_DIR) andSCF_SECRET_KEY— the in-app JSON backup never contains them.SCF_SECRET_KEYencrypts the integration credentials stored in the database, so a database restored onto a host that does not hold the same key cannot read its encrypted integration credentials. Back the credentials up withscripts/backup.sh(below). On a legacy install whose credentials are still in.env, back up.envseparately
Host-level backups (self-hosted)
Section titled “Host-level backups (self-hosted)”On a self-hosted install, scripts/backup.sh takes one complete backup set into ./backups. It uses
the same file names as the pre-upgrade backup taken by scripts/upgrade.sh, so either set can be
restored with scripts/upgrade.sh --rollback <TS>:
| File | Contents |
|---|---|
<TS>_v<version>.dump | Whole-database Postgres dump, including Keycloak’s schema and the audit log |
<TS>_v<version>_minio.tgz | MinIO evidence volume. Only on an install that bundles an object store — see Installs that bundle no object store |
secrets-<TS>.tar.gz | Every credential file from SCF_SECRETS_DIR, written with mode 0600. The wizard’s .provision-token is excluded: it authorises the installer and has no restore value |
<TS>_ref.txt, <TS>_checksums.sha256 | Git ref at backup time, and checksums of the data files |
scripts/upgrade.sh --rollback <TS> restores both data stores and, when secrets-<TS>.tar.gz is
present, unpacks it into SCF_SECRETS_DIR after setting the current credentials aside — so a
rolled-back database and the key that encrypted it move together.
The set is also the rollback point an upgrade carries with it. An upgrade hands over to the target
release’s copy of the script after checking the tag out, and the process that takes over locates
<TS>_v<version>.dump and <TS>_ref.txt on disk and refuses to build or migrate until it has
found them. So a set you move or delete mid-upgrade stops the upgrade rather than quietly costing it
its ability to roll back. Copy sets out of ./backups, never out from under a running upgrade.
Installs that bundle no object store
Section titled “Installs that bundle no object store”Installing with scripts/install.sh --no-minio bundles no object store: evidence lives in the
S3-compatible bucket you configure under Settings, Evidence storage, and there is no minio_data
volume on the host to tar.
Both scripts detect this and skip the evidence half of the set, loudly, rather than failing or
producing a tarball of nothing. The test is a non-empty MINIO_ROOT_USER, read from .env or from
SCF_SECRETS_DIR — the --no-minio installer writes that credential file deliberately empty, and
the bundled MinIO cannot start without one, so an empty value really does mean “this install has
never run a bundled object store”. On such an install:
scripts/backup.shprintsSKIPPING the evidence backup: no bundled object store on this install, and the set it writes is the database dump plus the credential tarball only. The_minio.tgzfile is absent, and the checksum file lists only the files that exist.scripts/upgrade.shprints the same warning and proceeds with the upgrade. Its pre-upgrade backup covers the database only, so a subsequent--rollbackrewinds the database and leaves your external store exactly as it is.- Neither script starts a MinIO container. Naming a profiled service on a
docker compose upcommand line activates its profile, so both scripts omitminiofrom that command when the storage profile is off.
An install that does bundle an object store is unaffected: the evidence tar stays mandatory and symmetric with the database dump, and both scripts still refuse to continue without it.
External database and external object storage
Section titled “External database and external object storage”External database. scripts/backup.sh runs docker compose exec -T postgres pg_dump and
derives the user and database from the compose postgres service, not from your DB_HOST /
DB_NAME / DB_USER. Because the bundled postgres service starts on every install — including
the external one — that command succeeds, produces a non-empty dump of the unused bundled
database, passes validation and reports success. scripts/upgrade.sh shares this logic, including
its --rollback restore path.
If you run an external database you must take your own backups, using your provider’s mechanism
(RDS snapshots, Cloud SQL backups, pg_dump against your host) or your own scheduled job. Verify a
restore end to end before you rely on it.
External object storage. scripts/backup.sh captures evidence by tarring the minio_data
Docker volume. The volume is the only thing it can tar, so a store that is not that volume is never
in the set — but the script now tells you when that is the case, in two different ways.
- No bundled store at all. An install made with
--no-minioskips the evidence tar loudly, as described above. - Organisations on their own stores. On every run, bundled or not, the script counts the organisations whose evidence storage configuration points somewhere of their own and warns that their files are outside the set. A bundled install is not covered merely because its volume was tarred: each of those organisations keeps its evidence somewhere this script has never heard of. When the count cannot be taken — an older schema, or Postgres unreachable — it says so rather than reporting zero.
One gap remains, and the warnings above do not close it: an install that bundles MinIO and has had
its platform-wide store repointed at an external bucket, by setting AWS_ENDPOINT_URL to real S3
in .env while MINIO_ROOT_USER is still set. There the script tars the now-unused MinIO volume,
reports success, and your live evidence is not in the backup set.
For any store that is not the bundled volume, use bucket versioning, cross-region replication or your provider’s backup tooling, and do not read a green backup run as covering it. Settings, Evidence storage names the provider and bucket for each organisation, which is where to start that list.
Bundled Keycloak with an external database. Keycloak’s connection is pinned to the bundled
Postgres (KC_DB_URL is a literal in docker-compose.yml) and does not follow DB_HOST. Running
the idp profile alongside an external application database therefore splits your data across two
servers: identity in the bundled container, application data in your external instance. Use an
external OIDC provider instead, or keep the bundled database.
Rotating DB_PASSWORD on an external database. The procedure in
Credentials and secrets targets the bundled
container. On an external database, run the ALTER ROLE against your own server instead — see the
note there.
Backup Best Practices
Section titled “Backup Best Practices”Recommended Backup Schedule
Section titled “Recommended Backup Schedule”| Frequency | When | Purpose |
|---|---|---|
| Weekly | End of week | Regular checkpoint |
| Before major changes | As needed | Safety net before bulk operations |
| Before audits | Before audit period | Preserve audit-ready state |
| Monthly archive | First of month | Long-term retention |
Storing Backups Securely
Section titled “Storing Backups Securely”- Keep multiple copies — Store backups in at least two locations
- Use secure storage — Encrypt backup files or store in secure cloud storage
- Document your backups — Keep a log of what each backup contains
- Test periodically — Verify backups can be restored successfully
Before Major Changes
Section titled “Before Major Changes”Always create a backup before:
- Bulk importing or deleting controls
- Major configuration changes
- User cleanup operations
- Organisation restructuring
Testing Your Backups
Section titled “Testing Your Backups”Regularly verify backups are restorable:
- Monthly test — Download a backup and review its contents
- Quarterly drill — Restore to verify data integrity
- After major updates — Create and verify a new backup
Verifying Backup Contents
Section titled “Verifying Backup Contents”After downloading a backup:
- Open the JSON file in a text editor
- Check
metadata.table_countsmatches expected record counts - Verify
metadata.created_atshows the correct timestamp - Spot-check a few records in the
datasection
Disaster Recovery
Section titled “Disaster Recovery”If Data Is Lost or Corrupted
Section titled “If Data Is Lost or Corrupted”- Stop writes. Take the stack down or stop the backend and workers so the damage stops spreading and the restore is not racing live traffic.
- Identify the scope. Which organisations and which tables are affected decides which mechanism you need — see the table below.
- Choose the mechanism. A host-level backup set is the only complete recovery path. The in-app export can only return the 12 tenant tables it carries, for the organisations it carries.
- Restore.
scripts/upgrade.sh --rollback <TS>restores both data stores and, when the credential tarball is present, the credentials that decrypt them. - Verify. Confirm row counts, then confirm evidence files actually open — a database restored
without its
SCF_SECRET_KEYcannot read its encrypted integration credentials, and evidence links signed with a differentDOWNLOAD_TOKEN_SECRETwill not resolve.
Which mechanism recovers what
Section titled “Which mechanism recovers what”| Scenario | Mechanism | Notes |
|---|---|---|
| Whole installation lost, or data corruption | scripts/upgrade.sh --rollback <TS> | The most complete path: database + evidence + credentials. It does not restore the catalogue JSON — see the row below |
| SCF catalogue JSON missing after a fresh-host restore | Re-run the catalogue importer against your own workbook | No backup contains it. Restart backend and celery-worker afterwards — see the caution above |
| Reverting a bad upgrade | scripts/upgrade.sh --rollback <TS> | upgrade.sh takes this backup automatically before it migrates |
| One organisation’s working data needs re-seeding from another install | In-app restore | 12 tables only; users and organisations are never deleted |
| Accidental deletion inside the 34 excluded tables (evidence, audit log, assessments, vendors) | scripts/upgrade.sh --rollback <TS> | The in-app export never carried these |
| External database or external object storage | Your provider’s tooling | Not covered by either mechanism — see above |
Recovery time depends on database size and evidence volume; measure it on your own install rather than assuming. Test the restore path before you need it — see Testing Your Backups.
Troubleshooting
Section titled “Troubleshooting”Backup Button Disabled
Section titled “Backup Button Disabled”Cause: Another backup/restore operation is in progress.
Solution: Wait for the current operation to complete.
“Invalid backup file” Error
Section titled ““Invalid backup file” Error”Causes:
- File is not valid JSON
- Missing
metadataordatasections - Unsupported backup version
Solutions:
- Verify the file wasn’t corrupted during download or transfer
- Check that you selected an SCF backup file (not some other JSON)
- For older backups, check version compatibility
Restore Fails Mid-Process
Section titled “Restore Fails Mid-Process”Cause: Network interruption or server error during restore.
Solution:
- Wait a moment and try again
- If issues persist, contact support
- Try restoring with a different backup file
Restore Shows Wrong Data
Section titled “Restore Shows Wrong Data”Cause: Restored wrong backup file.
Solution:
- Check the backup file’s
metadata.created_attimestamp - Restore the correct backup
- Keep backups organised with clear naming
Related Guides
Section titled “Related Guides”- Configuration — Platform settings
- Monitoring — System health
- Troubleshooting — Common issues

