Backup and Recovery
Protect your Syncropel data, including the database, task content, and configuration.
This page is for people running their own workspace or using the API. If you use a workspace created at syncropel.com, start with Your hosted workspace.
What to Protect
| Data | Location | Contains |
|---|---|---|
| Database | ~/.syncro/hub.db | All records, trust scores, routing rules |
| Task content | ~/.syncro-data/tasks/ | Task descriptions and artifacts |
| Aliases | ~/.syncro-data/aliases.toml | Human-readable task alias mappings |
| Config | ~/.syncro/config.toml | Identity, store URL, server settings |
Automatic Backup
The spl serve instance backs up the database on every startup, into a per-instance directory keyed by the instance DID:
~/.local/share/syncropel/backups/instance-<did-tail>/hub.db.bak<did-tail> is the 24-hex tail of your instance DID. To find your directory:
curl -s localhost:9100/health | jq -r .instance_did
ls ~/.local/share/syncropel/backups/instance-<tail>/The daily backup job writes timestamped hub.db.<YYYYMMDD-HHMMSS> copies into the same directory. (An instance running on an isolated SYNCROPEL_HOME before its identity is bootstrapped keys the directory as home-<hash>/ instead.)
The backup directory is OUTSIDE ~/.syncro/: removing that directory does not affect the backups.
The identity is part of the workspace, and only a sealed backup carries it
A backup of hub.db alone is a backup of records. The signing keys live
beside the store, not in it, and a restore without them boots as a DIFFERENT
workspace: a drill on 2026-07-20 restored 89,152 records into a body that
answered every authenticated route with BOOTSTRAP_REQUIRED. With
SPL_BACKUP_PASSPHRASE set in the daemon's environment the daily backup
seals the keys into the same set (spl backup --auto restore-keys brings them
back), and the doctor's daily backup row and the workspace read say so:
identity covered, or identity NOT covered with the reason and the last
time a backup did cover it. A body that has never covered its identity warns
even when its backup is fresh, because the thing it would bring back is not
the workspace. The keys recovery window is bounded by the last successful boot
that ran a backup, which is an older watermark than the last record flush;
read both.
Manual Backup
# Create a timestamped backup (use your instance directory from above)
cp ~/.syncro/hub.db ~/.local/share/syncropel/backups/instance-<tail>/hub.db.$(date +%Y%m%d-%H%M%S)
# List available backups across all instance directories
ls -la ~/.local/share/syncropel/backups/*/Recovery
# Stop the instance
spl stop
# Restore from backup
cp ~/.local/share/syncropel/backups/instance-<tail>/hub.db.bak ~/.syncro/hub.db
# Restart: trust and config rebuild automatically from records
spl serve
# Verify
spl status
spl task list
spl trustRestoring onto a new machine: the store first, then the keys
On a new machine, put the backed-up store in place first, then bring the keys back:
# 1. Put the store copy in place (with the daemon stopped)
cp <backup>/hub.db.<timestamp> ~/.syncro/hub.db
# 2. Then restore the keys from the same backup set
# (SPL_BACKUP_PASSPHRASE set to the backup's passphrase)
spl backup --auto restore-keys <backup>/keys.<timestamp>Do it in this order. If you run restore-keys before the store is in place, the workspace signs with your
original keys but can report the new machine's workspace name on /health. If that happened, put the store
in place and run restore-keys again with --force.
From v0.270 a backup set also holds the workspace's order log (order.<timestamp>, beside keys.<timestamp>).
restore-keys puts it back for you. If you copy files by hand instead, copy order.<timestamp> too: a store
that held an order log and boots without one refuses to start (ORDER_LOG_MISSING) rather than quietly
beginning a new history. If the set really has none, start with SPL_ACCEPT_NO_ORDER_LOG=1: the new log's
first entry records the gap.
Portable Backup: Publish the Instance
From v0.118, the recommended portable backup is a published repository snapshot of the whole instance:
spl repo publish <namespace>/<repo> --relocation--relocation is shorthand for --visibility L3 --scope instance --no-listing --carry-tokens: it snapshots the instance as a verified, portable repository tree. To rebuild a working instance from it on any machine:
# Client-side restore (with the daemon stopped)
spl repo restore <dir-or-https-url>
# Or hydrate a fresh home at boot
spl serve --repo <locator>The startup .bak file protects against local file loss; the published repository is the canonical portable recovery form: it travels between machines and verifies its own integrity on restore.
The open tail is verified, or refused by name
A tree's sealed segments were always verified on restore. Since v0.235 the
open tail (log/open/current.jsonl, the records since the last seal) is too:
the writer records the tail's hash in refs/HEAD, measured from the bytes in
the store, and spl repo restore, spl repo mount and spl repo drift read
the tail through one verdict, reported as tail.status:
| Status | Meaning |
|---|---|
verified | HEAD vouched for the tail and the bytes matched; its records were taken. |
refused | The fetch, the hash or the parse failed against HEAD's claim. Nothing from the tail was taken, the sealed set was restored alone, tail.reason names why and tail.not_taken says how many lines stood behind it. One well-formed record appended to the tail by anyone who can write one object into the prefix is exactly this case: content-addressing says nothing about who wrote a whole object. |
unverified | The tree was written before v0.235 and HEAD carries no tail hash; the caller passed --accept-unverified-tail and the lines were taken with the verdict on the report. Without the flag such a tail is refused. |
absent | HEAD names no open segment. |
The price of refusing to trust an unvouched tail is bounded by one flush interval: a different body re-basing a dead writer's tree does not carry records that lived only in a tail HEAD could not vouch for.
A copy older than your live workspace is refused unless you say so
From v0.272, spl repo restore checks the copy against the live workspace before anything lands. If the copy
is older, the restore stops and names both points, so an old copy can't quietly undo newer changes, such as
a key you removed:
spl repo restore <locator> # refused: the live workspace is newer
spl repo restore <locator> --accept-older # proceed; the restore records that you chose thisThe same check runs for spl repo mount, spl move, spl backup restore-keys and spl import. The one exception is
restoring keys from a daily backup set: a key set is always older than the live workspace, and restoring it
is the normal recovery, so it is noted rather than refused. Whatever you restore, anything you revoked or
removed stays revoked or removed: the lists of revoked keys and erased people are joined, never replaced.
| Code | What it means |
|---|---|
LIVE_TREE_NEWER | The live workspace is newer than the copy. Nothing was restored. |
Your task notes travel with the backup
From v0.272, task notes (~/.syncro-data/) travel with the off-machine copy, and the startup backup writes
data.bak.tar.zst beside hub.db.bak. Restore them on their own with:
spl backup restore-data <backup-set> # adds what's missing; refuses a differing file
spl backup restore-data <backup-set> --replace # saves the current notes first, then replacesrestore-data never overwrites a note that differs from the backup unless you pass --replace, and then it
saves a copy under ~/.local/share/syncropel/backups/tasks/ first. spl repo restore and spl move bring
the notes into an empty data folder; otherwise they tell you the command to run.
| Code | What it means |
|---|---|
DATA_CONFLICT | A note in the backup differs from yours. Every conflict is listed and nothing was written. |
What Rebuilds on Startup
The engine reconstructs all derived state from the immutable record log:
- Trust scores: replayed from outcome records
- Engine config: replayed from configuration records
- Thread state: derived from records (
state = fold(records))
Prevention
- Use separate ports for development (
--port 9200) and production (port 9100) - Development instances use
--memoryfor ephemeral storage - Task content files in
~/.syncro-data/are independent of the database
Tiers and entitlements
Every run is clamped by the tier of the principal it runs for. Tiers are records you can edit without a release, per-actor ceilings only ever tighten, and a subscription status maps to a tier through one kernel decision. A self-hoster and operator guide.
Body-Kind Manifests
Declare which body fields for a given body.kind should be indexed. The instance creates SQLite expression indexes at config reload so rich-query filters on nested body fields stay fast as your record log grows.