State-in-backend: instance state lives encrypted in the storage backend, not on local disk #138
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/Cairn#138
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Decided with Nikola (2026-08-07). This is the v0.6 milestone's core issue — design first, then implementation.
Motivation
Dogfooding v0.4.x surfaced that every runtime state file dies with
/data. On the dogfood,/datawas anemptyDir, so each redeploy silently reset:license-state.json(the "my license disappears after updates" report),settings.json(incl.peeringEnabled),shares.json(every share link died),holds.json(legal holds silently released by a version bump — the worst item for our compliance story),peering.json(trust state incl. live peer credentials),app-passwords.json,local-users.json. The dogfood is patched with a PVC (homelab-configcairn-enc-data), but every customer who runs the container without persistence has the identical failure mode waiting.The deep fix: stop keeping instance state on local disk. Precedent is MinIO, which stores its whole configuration and IAM in a hidden
.minio.sysprefix inside the object store it serves — the server binary is stateless, and pointing a fresh server at an existing volume resurrects the instance completely.Design
Each atomic-JSON store reads/writes
.cairn-state/<name>.jsoninside the configured storage backend instead of/data/.cairn/<name>.json:.cairn-state/prefix in the bucket. The prefix must be invisible to and unreachable from all user-facing surfaces (WebDAV, UI listing, shares, peering delivery) — same treatment as other internal paths..cairn-state/under the data root. These deployments already persist the data root, so they get durability for free and keep true rename-based atomicity.peering.jsonholds live credentials (keys peers issued us) andlocal-users.jsonholds verifiers; a bucket compromise must not yield either in the clear.The config/state line becomes exact:
config.json(+ its secret) answers "where is my data, how do I unlock it, who authenticates my users" — backend connection, encryption/recovery identity, IdP. Everything else is state and lives with the data. Corollary: the backend connection itself can never move into runtime settings (the pointer to storage can't live in the storage), which settles the "connect S3 from the settings panel" question — that surface is a read-only card plus, later, a first-run wizard and a supervised migration tool, never a live toggle.Stays local (genuine scratch): tus upload spool, peering blob staging (
peer-blobs). Already memory-only and staying that way: challenges, spent tokens, in-flight transfer records.Questions the design pass must answer
main.go, including the failure mode "backend unreachable at boot" (serve degraded? refuse to start? retry loop with health probe implications?).CAIRN_LICENSE_KEYalready wins per config.go) for the fresh-instance/DR path — it becomes a small corner of this issue instead of its own feature.docs/handbook/deployment.mdaccordingly, including the sharp-edge table of what was previously local-only.Acceptance
/dataloses nothing.age -dwith recovery key) works for state exactly as it does for file data..cairn-state/.Milestone scaffolded 2026-08-09 (v0.5 closed the same day — v0.5.0 is live on the dogfood). This issue's seven design questions are now an issue chain, v0.5-style: a gating ADR first, everything else hangs off it. This issue stays open as the milestone umbrella and closes last, with #159.
.cairn-state/invisible/unreachable from every user surface incl. unscoped adminsSuggested order: #157 (small, independent, customer-facing win immediately) → #152 → #153 → #155 ∥ #156 → #154 → #159 → #158.
Scaffold amended 2026-08-09: all milestone-wide design decisions are ratified in one pass — the ADR lives on #152 (D1–D11). New in scope: #160 — storage connect from the WebUI (bootstrap layer,
storage.driver: "setup"mode, wizard with probe→persist→graceful-restart), which amends the config/state line into the three-layer model: the pointer to your data lives locally; everything it points to lives with the data.Updated chain: #157 (independent, first) → #152 (skeleton; design done) → #153 → #155 ∥ #156 → #154 → #160 → #159 (acceptance, now also covers setup-mode + wizard) → #158 (docs, incl. bootstrap/setup-mode sharp edges). With decisions pre-made, implementation is planned as three large sessions: (1) #157 + #152, (2) #153 + #154, (3) #155 + #156 + #160 — then acceptance and docs.
Umbrella closed — every acceptance criterion executed, not just met.
Localstore is byte-identical to pre-v0.6 files (zero-migration path, unit-proven)..cairn-state/settings.json.agefrom Garage and opened it offline with the recovery key; a wrong key fails. Same drill as file data, now documented next to it..cairn-stateverified invisible from the live admin UI; the lock object heartbeats in the bucket.Design questions from this issue, all settled and recorded in the #152 ADR (D1–D11): whole-object PUT without CAS (D2), boot ordering storage-first with refuse-on-unreachable (D5), synchronous write-through (D6), refuse-on-conflict migration (D7), the advisory lock adopted with honest limits (D8), license re-activation folded in (#157/D10), and the backup story rewritten (#158). Beyond the original scope, the milestone also delivered storage-connect-from-the-browser (#160: bootstrap layer, setup mode, wizard) and two rounds of UX hardening the live walkthrough surfaced (#166, #168).
Shipped as v0.5.1–v0.5.7; milestone closes as v0.6.0. The config/state line, final form: the pointer to your data lives locally; everything it points to lives with the data.