CRUMB a card from devarno-cloud

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:

  1. Airlock session → resolves to admin / operator / observer / denied.
  2. HATCH audit event → written before the action takes effect.
  3. 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
end

Build-mode matrix

ModeROCKY_AUTHROCKY_BILLINGAirlockPolar
CloudairlockenabledBetterAuth on .devarno.cloud cookieenforced for tier > solo
Self-host (default)localdisabledLocalAuth 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):

LifecycleStarted eventOutcome events
provisionhearth.provisioning_startedhearth.provisioned / hearth.failed
upgradehearth.upgrade_startedhearth.upgraded / hearth.failed
decommissionhearth.decommission_startedhearth.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.cloud cross-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