Skip to content

Storage backends

Rivet v0.4 separates what a snapshot is (a versioned, reviewable tree of manifest + files) from where it lives, per ADR-0002. Git stays the reviewable default; this is the reference for the other three: local/NAS (fs), the long tail of cloud/NAS providers (rclone), and deduplicated backup history (restic).

Backend Best for Extra dependency Encryption
git (default) Reviewing every change in a diff before it happens none none (plaintext, reviewable by design)
fs A NAS share already mounted over SMB/NFS, or a second local disk none Rivet-side age, on by default
rclone S3, Backblaze B2, Google Drive, Dropbox, SFTP, WebDAV, and ~70 other providers rclone, already configured Rivet-side age, on by default
restic Deduplicated, incremental backup history without Git restic restic’s own (Rivet’s encrypt flag does not apply)

You can configure more than one — snapshot writes to every configured backend (fan-out); restore reads from the first configured one, or a specific one via --backend <id>.

Each backend is a repeatable [[backend]] block in the snapshot repository’s rivet.toml, added with rivet init --backend <id> ...:

Terminal window
# Local/mounted NAS share, encrypted by default
rivet init --repo ~/rivet-snapshots --backend fs --path /mnt/nas/rivet-snapshots
# rclone remote (configure the remote itself with `rclone config` first)
rivet init --repo ~/rivet-snapshots --backend rclone --remote b2:my-bucket/rivet
# restic repository, password from an env var you already set
rivet init --repo ~/rivet-snapshots --backend restic \
--restic-repo sftp:nas:/backups/rivet --password-env RESTIC_PASSWORD
# Skip Rivet-side encryption for fs/rclone (e.g. you're already using
# `rclone crypt`, or the destination is already encrypted at rest)
rivet init --repo ~/rivet-snapshots --backend fs --path /mnt/nas/rivet-snapshots --no-encrypt

Run init --backend again with a different --backend/target to add a second destination — rivet.toml accumulates one [[backend]] entry per call. If no [[backend]] is ever added, Rivet behaves exactly as it did through v0.3: Git only.

Terminal window
rivet snapshot # writes to every configured backend
rivet restore # reads from the first configured one
rivet restore --backend restic # reads from a specific one

An unavailable optional tool (rclone/restic not installed, or not configured) only breaks the backend that needs it — a snapshot to your other configured destinations, or to Git, still succeeds. This is the same availability-check pattern snap/flatpak have used since v0.1.

fs and rclone wrap every blob they write — tracked file/AppImage/hook content, manifest.toml, and the readable packages/ lists — with age before it touches disk, by default (encrypt = true; pass --no-encrypt to skip it). Directory structure and file names stay in cleartext; only each blob’s bytes become opaque.

The first init --backend fs --encrypt (or rclone) generates an age X25519 keypair:

  • The public recipient (age1...) is stored in rivet.toml.
  • The private identity is written to ~/.config/rivet/backends/<id>.age (mode 600) — never anywhere under the backend’s own storage path. Losing this file means losing access to that backend’s encrypted snapshots; back it up somewhere separate from the snapshots themselves.

Running init --backend fs --encrypt again reuses the existing identity at that path rather than generating a new one and silently orphaning anything already encrypted to the old key.

restic owns its own repository encryption end to end — Rivet’s encrypt flag has no effect there. The repository password is never written to rivet.toml in plaintext: point --password-env <VAR> at an environment variable you already set (Rivet reads it from its own environment and forwards only the value to restic), or --password-file <path> at a file restic reads directly.

v0.5 note: this is a single-recipient, no-rotation primitive by design (ADR-0002 §4) — multi-recipient support and key rotation are the v0.5 secrets vault’s job (ADR-0003), built over this same age primitive rather than a second crypto path.

  • fs: <path>/snapshots/<utc-timestamp>/ (manifest.toml + files/
    • appimages/ + hooks/ + packages/), one directory per snapshot. Change-detection compares a SHA-256 hash of the whole tree against the latest snapshot’s and skips the write when nothing changed — the same “commit only on change” behaviour as Git.
  • rclone: identical shape, staged locally through the same fs logic (including encryption) and pushed with rclone sync/rclone copy. Change-detection fetches only the remote’s small hash file rather than downloading a whole snapshot to compare.
  • restic: one restic backup per snapshot, tagged rivet-hash:<sha256> so a repeat snapshot with nothing changed is detected and skipped — restic itself always creates a new snapshot entry per backup call regardless of content (dedup happens at the storage layer, not the snapshot-count layer), so Rivet’s own tag is what restores the “only on change” behaviour.

rclone’s :local: connection string and a throwaway local restic repository need no cloud credentials or rclone.conf entry, so CI installs both and runs the full round-trip suite (cargo test -- --include-ignored) against them on every push — see .github/workflows/ci.yml.