Decision 0012 — Litestream as an optional fast-RPO sidecar
Litestream as an optional fast-RPO sidecar replicating the database WAL, with restic staying authoritative and an unencrypted remote refused without an acknowledgement.
Status
Accepted · 2026-08-22 — complements the restic DR chain in operations/disaster-recovery.md; does not supersede it.
Context
The default DR chain is three restic legs plus a 15-minute DB-only fast path
(disaster-recovery.md). That fast path is a VACUUM INTO and a restic backup of
exocomp.db on a timer: cheap, encrypted, drill-covered — and bounded below by its
interval. An operator who cannot accept losing up to 15 minutes of job history wants
a tighter RPO than a timer can give, without a second heavy service inside the
container.
Litestream ships a SQLite database's WAL frames to a replica target continuously, giving seconds-level RPO. An earlier design draft deferred it; review flagged the one property that constrains how it can be adopted here: Litestream does not encrypt. It streams WAL frames to the target as it reads them. That is safe to a LAN-local disk or an already-encrypted store, and unsafe to a plaintext bucket over the open internet.
Two rules from the existing architecture bound the decision:
- restic stays authoritative (invariant 7; disaster-recovery.md). DR must not
depend on a vendor or on this project's code, and the encrypted, drill-covered
chain a bare host recovers from is restic. Litestream cannot become the thing the
recovery card derives, because its replica is not encrypted and its restore is not
the manual
age/openssl/resticpath the card guarantees. - no second heavy service inside the daemon (Decision 0001). The thin core owns state,
sequencing, keys, and recovery, and shells out to pinned tools for everything else.
A backup engine compiled into
exocompdwould violate the one hard rule of Decision 0001 as surely as an embedded Terraform executor would.
Decision
Adopt Litestream as an OPTIONAL sidecar that replicates exocomp.db's WAL for
seconds-level RPO — the fast-RPO complement to restic, never its replacement.
-
Sidecar, not embedded. Litestream runs as its own process beside
exocompd, like restic and tofu are their own processes.exocompddoes not link it, compile it in, or spawn it from inside the daemon. It generates thelitestream.ymlconfig (internal/litestream.GenerateConfig, deterministic) and observes the replica's freshness; the operator (or a Quadlet/compose unit) runs the binary. A host without the binary degrades honestly:exocomp litestream statusandexocomp doctorreport it absent and name the one install step, and the freshness gauge reads +Inf ("unknown"), never a misleading zero. -
restic stays the authoritative encrypted DR chain. Litestream tightens RPO from ~15 min to ~1 s for operators who want it; it changes nothing about restore. The recovery card, the drill, and the three legs are untouched. Losing 15 minutes of job history is not a data-loss event worth a second process for most operators, so the sidecar stays off by default.
-
The no-encryption caveat is enforced, not documented. A replica target must be LAN-local (a path,
file://, an NFS/SMB mount) or an already-encrypted store. A remote target (s3://,sftp://) is refused atlitestream targetand at config generation unless the operator passes--ack-unencrypted, which asserts the endpoint is encrypted or the plaintext risk is accepted. The acknowledged config carries the warning in its own header, so the caveat is legible in the artifact. -
Credentials go through the sealed secret plane. A remote target's credentials are never written into the emitted config. Litestream reads them at launch from
LITESTREAM_ACCESS_KEY_ID/LITESTREAM_SECRET_ACCESS_KEY(and the SFTP equivalents), which the sidecar's launch unit populates from the sealed secret plane — the same seam restic's s3 leg uses. The generated config documents this at the replica block rather than leaving a plaintext-credential temptation.
Consequences
- An operator who needs it gets ~1 s RPO by installing one binary and pointing a Quadlet/compose unit at the generated config; an operator who does not is unaffected and pays for no second process.
exocomp doctorgains a litestream section (binary presence, configured target, RPO age) and/metricsgainsexocomp_litestream_replication_staleness_seconds— the RPO age, +Inf when unknown — beside the other staleness gauges, so a silently stopped sidecar drifts an alert the same way a stopped backup leg does.- Config generation is deterministic and golden-tested, and freshness for a file replica is read from the replica's own layout, so the whole feature is unit-tested with no litestream binary present; any e2e that shells the binary SKIPs cleanly when it is absent, like the tofu/ansible gates.
- The encryption caveat is a build-enforced refusal, not a footnote: a plaintext remote target cannot be persisted or rendered without the explicit ack, closing the most likely way to leak an unencrypted database off-LAN.
- Litestream is a new optional entry in the bill of materials (already listed as an optional sidecar); it is Apache-2.0 and crosses a process boundary, so it raises no license-boundary question (bill-of-materials.md).
- Restore stays restic's job. The
litestream restorefast path (internal/litestream.RestoreArgs) exists for catching a replica up, but the authoritative "control host is gone" runbook is unchanged: it derives the restic password from the DEK and restores the encrypted chain.