Operations · 9. Backup and recovery
Back up the whole data directory. If you can only take one file, take passport.key, and understand that a backup without it is not a backup. This chapter is a how-to.
The disk layout itself is chapter 6 of the internals set. This chapter is what to do about it.
9.1 The one-sentence policy
The daemon's entire state is one directory. Back that directory up and you have backed up the daemon. Delete facts.jsonl from it and the daemon starts clean, healthy and empty, and will not tell you anything is missing.
That last clause is why this chapter is not optional. There is no alarm for "your memory is gone". /readyz returns 200 on an empty store.
9.2 What to copy
Take the whole directory. It is designed for that: temp-file-and-rename everywhere, fsync before rename, and a MANIFEST that is the authority for which segments are live.
If a constraint forces a partial copy, the minimum viable set is:
| Path | Why it is in the minimum set |
|---|---|
facts.jsonl | The memory itself |
passport.key | The daemon's identity. See §9.3 |
passports/ | The passport records |
CONTROL.json | Valve and control state |
meta/ | Routing and shard-map metadata |
shards/ | The event store |
audit-export-signing.key | Audit export signing |
integrations/ | Installed integration state |
.install-uuid | Install identity |
Do not copy the LOCK while a daemon is running. It is regenerated on start.
9.3 Why passport.key is the file that matters
passport.key does two jobs, and losing it breaks both irreversibly:
- It is the daemon's signing identity. Receipts written under the old key will not verify against a restored daemon that generated a new one.
- It encrypts stored integration credentials through a derived subkey. Lose the key and every stored connector credential is unrecoverable ciphertext.
There is no recovery path. If the file is absent at boot, the daemon silently generates a new one and carries on. The first symptom you will see is receipts that stop verifying and an identity you do not recognise (chapter 8 §8.5).
Back it up separately, before anything writes. It is small, it is static, and it is the only file in the system whose loss cannot be worked around.
9.4 Taking a copy that is actually consistent
Four rules.
1. Stop the daemon, or accept a fuzzy snapshot. There is no quiesce command. The journals are append-only and fsynced, and segment writes are atomic, so a running copy is usually consistent, but "usually" is not a backup policy for the fact journal.
2. Filesystem snapshots are the good answer where you have them. The layout was designed for snapshot semantics; take one and copy from it.
3. Never back up only facts.jsonl. Without passport.key, receipts written by the old identity will not verify against the restored daemon. A journal-only backup restores the data and destroys the proof.
4. Order matters if you cannot stop the process. Copy passport.key and the small metadata files first, then the journals, then the shards. A journal that is ahead of its metadata replays; metadata ahead of its journal does not.
9.5 Proving a backup restores
A backup you have not restored is a hope. The test is small enough to automate and there is no excuse for skipping it.
# 1. Restore the copy to a scratch path.
cp -a /backups/corecrux-2026-07-27 /srv/restore-test
# 2. Start a daemon against it, on a spare port, loopback only.
CORECRUXD_DATA_DIR=/srv/restore-test \
CORECRUXD_AUTH_MODE=dev_scopes \
CORECRUXD_HTTP_PORT=24800 \
CORECRUXD_MCP_PORT=24801 \
CORECRUXD_GRPC_PORT=24007 \
corecruxd &
# 3. The gate: readiness, then one substantive read.
curl -s -o /dev/null -w 'readyz %{http_code}\n' http://127.0.0.1:24800/readyz
curl -s http://127.0.0.1:24800/v1/version
curl -s 'http://127.0.0.1:24800/v1/facts/list?limit=1'
The pass criteria, in order:
| Check | Pass means |
|---|---|
/readyz returns 200 | All nine gates pass on the restored directory |
/v1/version returns the expected build | You restored what you think you restored |
/v1/facts/list?limit=1 reports a total_nondeleted you recognise | The journal replayed and the store is populated |
| A receipt verifies | The identity survived. This is the check that catches a missing passport.key |
That last one is the whole point of the exercise. Open the restored daemon's console at Trust › Receipts, find a fetchable ad_ga_* row, expand it, and read the verdict. signature_valid: true with error_code: OK means the identity restored. Anything else means it did not.
Use different ports so the test never contends with production, and never point the test at the production directory; it would take the lock.
9.6 The restore procedure
1. Stop the daemon cleanly. SIGTERM, not SIGKILL. The drain cap is CORECRUXD_SHUTDOWN_DRAIN_SECS, default 30 seconds; 0 drains without bound (config.rs:144). A hard kill during a journal append is the one write path where the fsync discipline cannot help you.
2. Move the current directory aside rather than deleting it. You may need to reconcile.
3. Restore the copy in place.
4. Fix ownership. In a container the daemon runs as UID 65532 and create_dir_all on an unwritable data directory is startup-fatal. chown -R 65532:65532 the restored tree if that is your deployment shape.
5. Do not copy in a LOCK. If one came with the backup, delete it.
6. Start the daemon. The first boot after a restore replays every JSONL journal and rescans .ccxi companions, so it is slower than a warm restart. Set your startup probe's initial delay accordingly.
7. Verify, in this order: /readyz 200, then /v1/version, then one substantive endpoint, then a receipt verification.
9.7 What a restore does not bring back
Four things, stated as plainly as the rest:
| Not restored | Why |
|---|---|
| Device-authorization grants | Process-local, invalidated by any restart |
| In-memory session state | If CORECRUXD_FACT_PERSISTENCE was off, session state was never on disk |
The LOCK | Regenerated, and should not be copied while a daemon is running |
| Anything written after the copy | Obviously: but worth stating, because the daemon will not tell you there is a gap |
There is also no partial restore. The directory is one unit; restoring facts.jsonl from Tuesday into a Thursday directory produces a store whose journal and metadata disagree.
9.8 A backup schedule that is defensible
| Cadence | Action |
|---|---|
| Once, before anything writes | Copy passport.key to a separate, durable location |
| Daily | Filesystem snapshot, or a stop-copy-start if you can take the downtime |
| Weekly | Retain one daily as the weekly |
| Monthly | Run §9.5 against the most recent backup. Not a spot check, the full four-step gate |
| On every upgrade | Take a backup first. Chapter 10 §10.2 |
The monthly restore test is the item people drop, and it is the only item that distinguishes a backup from a directory of files.
9.9 What backup does not protect you from
Schneier's question, answered: what does this not prove.
- It does not protect against a bad write. The fact journal is append-only; a wrong fact written yesterday is in every backup taken since. Use the as of control in Memory › Facts to find when a value changed, and consolidation to supersede it.
- It does not prove the data was not tampered with. Receipts do that, and only for what they cover, a CROWN receipt is a verifiable record of what was stored and retrieved, which is tamper-evidence, not attestation of conduct.
- It does not survive losing the key alone. A complete directory backup with a separately-lost
passport.keyrestores the data and loses the proof. That is why the key gets its own copy, in its own place. - It says nothing about the hosted tier. If you rely on a mediated engine, its state is not in your data directory and this procedure does not cover it.
Sources
- crates/corecruxd/src/config.rs:144,
DEFAULT_SHUTDOWN_DRAIN_SECS - crates/corecruxd/src/config.rs:266,
CORECRUXD_SHUTDOWN_DRAIN_SECS - crates/corecruxd/src/http/health.rs:276, the readiness gates a restored directory must pass
- Dockerfile:60, the non-root UID a bind-mounted directory must be owned by
- /docs/daemon/06-data-directory, the full file-by-file map

