Skip to content

Backup & Restore

Backup and restore illustration

The platform offers two different things, and only one of them is a backup.

In-app exportscripts/backup.sh
PurposeMove one organisation between installationsDisaster recovery
Covers12 tenant tablesWhole database + evidence + credentials
Excludes34 org-scoped tables, incl. evidence and audit logSee what it does not cover
RestoreScoped upsert, users/orgs never deletedscripts/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.

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.

  1. Open Settings, Backups
  2. Click Download Tenant Export
  3. A JSON file downloads automatically, named scf-backup-YYYY-MM-DDTHH-MM-SS.json
  4. 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)
  1. Open Settings, Backups
  2. Click Import Tenant Data and select your export JSON
  3. 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
  4. Click Confirm Restore to proceed, or Cancel to back out

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": [...],
...
}
}
FieldDescription
metadata.versionBackup format version (currently 1.1; 1.0 files still restore)
metadata.created_atISO timestamp of backup creation
metadata.created_byUser who created the backup
metadata.table_countsRecord count per table
dataFull data for each table

Data TypeIncludedNotes
OrganisationsYesAll organisation records
Scoped ControlsYesControl selections and status
Evidence TrackingYesEvidence configurations
UsersYesUser accounts and roles
Organisation MembersYesUser-org relationships
AssignmentsYesControl/evidence assignments
CommentsYesAll comments and discussions
Evidence TasksYesTask records
NotificationsYesNotification history
  • 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) and SCF_SECRET_KEY — the in-app JSON backup never contains them. SCF_SECRET_KEY encrypts 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 with scripts/backup.sh (below). On a legacy install whose credentials are still in .env, back up .env separately

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>:

FileContents
<TS>_v<version>.dumpWhole-database Postgres dump, including Keycloak’s schema and the audit log
<TS>_v<version>_minio.tgzMinIO evidence volume. Only on an install that bundles an object store — see Installs that bundle no object store
secrets-<TS>.tar.gzEvery 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.sha256Git 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.

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.sh prints SKIPPING 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.tgz file is absent, and the checksum file lists only the files that exist.
  • scripts/upgrade.sh prints the same warning and proceeds with the upgrade. Its pre-upgrade backup covers the database only, so a subsequent --rollback rewinds 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 up command line activates its profile, so both scripts omit minio from 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-minio skips 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.


FrequencyWhenPurpose
WeeklyEnd of weekRegular checkpoint
Before major changesAs neededSafety net before bulk operations
Before auditsBefore audit periodPreserve audit-ready state
Monthly archiveFirst of monthLong-term retention
  1. Keep multiple copies — Store backups in at least two locations
  2. Use secure storage — Encrypt backup files or store in secure cloud storage
  3. Document your backups — Keep a log of what each backup contains
  4. Test periodically — Verify backups can be restored successfully

Always create a backup before:

  • Bulk importing or deleting controls
  • Major configuration changes
  • User cleanup operations
  • Organisation restructuring

Regularly verify backups are restorable:

  1. Monthly test — Download a backup and review its contents
  2. Quarterly drill — Restore to verify data integrity
  3. After major updates — Create and verify a new backup

After downloading a backup:

  1. Open the JSON file in a text editor
  2. Check metadata.table_counts matches expected record counts
  3. Verify metadata.created_at shows the correct timestamp
  4. Spot-check a few records in the data section

  1. 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.
  2. Identify the scope. Which organisations and which tables are affected decides which mechanism you need — see the table below.
  3. 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.
  4. Restore. scripts/upgrade.sh --rollback <TS> restores both data stores and, when the credential tarball is present, the credentials that decrypt them.
  5. Verify. Confirm row counts, then confirm evidence files actually open — a database restored without its SCF_SECRET_KEY cannot read its encrypted integration credentials, and evidence links signed with a different DOWNLOAD_TOKEN_SECRET will not resolve.
ScenarioMechanismNotes
Whole installation lost, or data corruptionscripts/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 restoreRe-run the catalogue importer against your own workbookNo backup contains it. Restart backend and celery-worker afterwards — see the caution above
Reverting a bad upgradescripts/upgrade.sh --rollback <TS>upgrade.sh takes this backup automatically before it migrates
One organisation’s working data needs re-seeding from another installIn-app restore12 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 storageYour provider’s toolingNot 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.


Cause: Another backup/restore operation is in progress.

Solution: Wait for the current operation to complete.

Causes:

  • File is not valid JSON
  • Missing metadata or data sections
  • Unsupported backup version

Solutions:

  1. Verify the file wasn’t corrupted during download or transfer
  2. Check that you selected an SCF backup file (not some other JSON)
  3. For older backups, check version compatibility

Cause: Network interruption or server error during restore.

Solution:

  1. Wait a moment and try again
  2. If issues persist, contact support
  3. Try restoring with a different backup file

Cause: Restored wrong backup file.

Solution:

  1. Check the backup file’s metadata.created_at timestamp
  2. Restore the correct backup
  3. Keep backups organised with clear naming