Backups

Backups often arrive as a sidecar after an application already depends on its data. That separation is especially dangerous for event sourcing because both history and current state carry meaning. Copying files can keep bytes without proving which transactions or payloads form one recoverable lineage. Stardust makes continuous verified backup part of the data design.

Recovery starts with logical history

A logical backup records each transaction as one packet: its id, commit time, field names, facts, and referenced binary payloads. It excludes derived indexes, storage pages, and the operational state of live mutations. After a logical restore, a live mutation re-derives from the beginning, because its output facts were restored as ordinary facts.

Each packet names the cursor of its predecessor, which pairs a transaction id with that packet's digest. The chain detects missing, reordered, substituted, or malformed history before recovery. A packet's canonical encoding defines its identity, and a decoded packet must re-render byte for byte. Values appear as legible JSON, with #-prefixed keys such as #sha256 for typed values.

Packets enter runs. A run is one immutable file of consecutive transactions with one packet per line. New runs target 1 MiB of packet bytes. Compaction merges a level once its bytes reach eight times the previous level's target, and it advances the manifest before it removes the replaced runs.

Committed transaction

Canonical digest-linked packet

Immutable run, optional zstd and AES-256-GCM

Referenced payload chunks

Content-addressed binary objects

manifest.json head

Verifiable recovery history

Definitions describe targets

A backup definition is an entity under definitions/backups/. It names exactly one file or s3 target:

dust297B
file             {path /var/backups/stardust}
compression      {algorithm zstd
                  level     3}
encryptionKeyEnv STARDUST_BACKUP_KEY
maximumLag       {transactions 100
                  logicalBytes 1000
                  binaryBytes  2000
                  duration     #dur(PT1H)}

Compression is opt-in. Without it, runs stay plain .jsonl files. With zstd, each run is one frame, so standard tools can read it. Encryption uses AES-256-GCM with a 32-byte key.

Continuous backup has a write boundary

Scheduled copies can stop for a long time while an application continues to create unprotected history. Stardust instead runs a backup supervisor that wakes every 100 milliseconds and wakes early when a backup definition changes. Each pass measures a target's lag, appends new transactions when it is behind, and compacts.

maximumLag describes four forms of exposure: transaction count, logical bytes, binary bytes, and the age of the oldest unprotected commit. Projects set the limits that match their workload. A measurement equal to its limit is permitted, while the first excess holds data writes.

Every target must satisfy its own limits. Writes are also held while any target's lag is unmeasured or its history is corrupted. A target that is unreachable holds data writes until it recovers, unless the mount started with --bypass-unreachable-backups. With that option, the limits continue to apply: the lag of a target that did not attach is measured from the last transaction it contains, which the database keeps across restarts, and from the start of history for a target that did not get a transaction. Backup definitions stay editable while writes are held.

Runtime status appears at runtime/backups.json. It reports whether writes are held, any definition reload error, one line per unreachable target, and each target's status: Starting, Synchronized, Retrying, or Corrupted.

Targets implement one small contract

File and S3-compatible targets share write-once objects and one replaceable head, manifest.json. The head lists every run and the tip cursor. Content digests verify each object independently.

A file target holds an advisory writer lock for its lifetime. It writes each object to a temporary file and renames it into place, and it replaces the head the same way. An S3 target publishes objects with If-None-Match and replaces the head with a conditional request. Both approaches keep a second writer from silently publishing an unrelated history under one target.

A target whose history is corrupted or divergent stops. Its status becomes Corrupted, and data writes stay held.

Binary closure follows facts

A packet lists every payload that its stardust/object/digest facts assert. The target keeps each payload's chunk manifest and chunks under content-addressed paths, so equal chunks are written once. Encrypted binary objects use a nonce derived from their content, which gives equal content one stable encrypted form.

Other #sha256 values are checksums and create no binary backup work. Payloads that no object fact ever referenced stay outside recovery material.

Policy is knowledge, credentials are local

Backup definitions are ordinary entities, so their revisions belong in canonical history with other consequential project decisions. They stay queryable without exposing the secrets that activate them.

A definition names environment variables rather than holding credentials or keys. Stardust reads those values from the environment when the mount starts. A backup manifest records which encryption algorithm its runs use, but not the key or the variable that held it.

Recovery refuses ambiguity

When a mount opens an existing database, each reachable target's history is validated and compared with local history. Restore appends only the suffix after an exact local prefix. Any difference in transaction ids or digests is divergence, and Stardust refuses it. Commit timestamps and source order never select a history.

When the database file is missing, the mount rebuilds it before anything writes to it. It uses --restore-from when given, or the default backups directory in the workspace. Restore writes a temporary file and renames it over the database path only after every packet lands. Encrypted history without a usable key, corrupt history, or an explicit source with no history stops startup instead of creating an empty database. --start-empty skips this discovery.

Each packet and its payloads publish in one database transaction. Stardust verifies every chunk before publication, so a restored fact never names unverified bytes. Missing or mismatched objects make the history corrupt.

Immutable objects, digest chains, and conditional heads make each backup transition inspectable. Lag limits bound accepted loss before failure, while recovery rejects histories that cannot prove their continuity.