Tenancy Invariant: Airlock + HATCH + Polar
rocky intermediate 6 min read
ELI5
Every Rocky action passes three doors before it touches state: an ID check (Airlock), a logbook entry (HATCH), and — only on the cloud build — a receipt check (Polar.sh). If the logbook fails, the action never happens; the receipt check is replaced by a dummy on self-host installs so OSS users never need a billing account.
Technical Deep Dive
The three checks (in order)
Per system-redesign §Tenancy invariant: no Rocky route, worker call, or driver action executes without:
- Airlock session → resolves to
admin/operator/observer/denied. - HATCH audit event → written before the action takes effect.
- Polar.sh entitlement check (cloud build only) → on provisioning, upgrade, and decommission of any tier >
solo. Per-request runtime checks are not required.
Order of operations
sequenceDiagram autonumber participant Client participant Route as console route participant Airlock participant Polar participant RELAY as SS-05 RELAY participant HATCH participant Driver as subsystem driver
Client->>Route: POST /api/hearth/workspaces Route->>Airlock: getServerSession() alt session denied Airlock-->>Route: denied Route->>RELAY: auth.denied RELAY->>HATCH: write Route-->>Client: 403 else session admin Airlock-->>Route: admin Route->>RELAY: hearth.provisioning_started alt RELAY write fails RELAY-->>Route: error Route-->>Client: 503 (driver NOT called) else RELAY->>HATCH: write Route->>Polar: assertEntitlement(slug, tier) Note over Polar: no-op when ROCKY_BILLING=disabled Polar-->>Route: ok Route->>Driver: Provision(...) Driver-->>Route: DeploymentRef Route->>RELAY: hearth.provisioned RELAY->>HATCH: write Route-->>Client: 200 end endBuild-mode matrix
| Mode | ROCKY_AUTH | ROCKY_BILLING | Airlock | Polar |
|---|---|---|---|---|
| Cloud | airlock | enabled | BetterAuth on .devarno.cloud cookie | enforced for tier > solo |
| Self-host (default) | local | disabled | LocalAuth adapter (file-backed single-user) | adapter compiles to no-op |
The Polar adapter is the only place Polar.sh code lives (console/src/lib/polar/entitlements.ts). Self-host builds never link cloud entitlement code paths — verified by the Phase 5 OSS-parity invariant in CI.
What the HATCH event records
The pattern is “started → outcome”. For HEARTH (Phase 5 §10):
| Lifecycle | Started event | Outcome events |
|---|---|---|
| provision | hearth.provisioning_started | hearth.provisioned / hearth.failed |
| upgrade | hearth.upgrade_started | hearth.upgraded / hearth.failed |
| decommission | hearth.decommission_started | hearth.decommissioned |
The “started” event is written before the driver call so a crash mid-driver still leaves a tombstone in HATCH. The outcome event is written before the route response so the API contract reflects the audited state.
The non-negotiable
Step 2 (HATCH) is non-negotiable in all builds. Step 1 is satisfied by LocalAuth in self-host. Step 3 is the only step that disappears off-cloud.
flowchart LR Req[mutating request] --> A{Airlock} A -- denied --> X[403 + HATCH auth.denied] A -- ok --> H{HATCH started} H -- write fails --> Y[503; driver NOT called] H -- written --> P{Polar?} P -- self-host --> D[Driver call] P -- cloud, no entitlement --> Z[402-equivalent] P -- cloud, ok --> D D --> O[HATCH outcome event] O --> R[200]Key Terms
- Airlock → BetterAuth-based session service running on
.devarno.cloudcross-subdomain cookies; the cloud auth source of truth - HATCH → audit event sink. Cloud build writes to a service; self-host writes to a local JSONL file
- LocalAuth → file-backed single-user identity adapter for
ROCKY_AUTH=local(resolved decision 0001 §5) - OSS parity invariant → solo + LocalDocker + LocalAuth must pass the full e2e with no Polar network calls
Q&A
Q: What happens if the “started” HATCH event write fails? A: The route returns 503 and the driver is never called. The “audit-event-before-action” half of the invariant is enforced by short-circuiting on RELAY failure.
Q: Why is Polar checked only on provision/upgrade/decommission instead of per-request?
A: Tier is enforced through the persisted DeploymentRef row plus the tier_downgrade_active_data policy (read-only mode 7 days, then admin decommission). Per-request checks would add latency for no extra safety once the row is already gated.
Q: What replaces Airlock when ROCKY_AUTH=local?
A: The LocalAuth adapter resolves to a single-user identity backed by a local file. The tenancy invariant still holds — the action is still authenticated, just to one identity.
Examples
A bank vault with three doors: card-reader (Airlock), CCTV that must record a frame before the bolt unlocks (HATCH started), and — only at cloud branches — a teller checking your account is in good standing (Polar). At a one-person home safe (self-host), the teller window is bricked over but the camera still rolls. If the camera is dead, the bolt does not move.
neighbors on the map
- Multi-Strategy Authentication debugging 401 errors across different clients
- Airlock JWT Handoff & Session Cookies debugging login loops or session expiry
- LORE RBAC & Airlock Auth Flow implementing authentication in a new LORE page
- Airlock Bearer Auth diagnosing a 401 from /v1/ingest/*