ADR 0201: Hosted on Cloudflare

Status: proposed, 2026-10-08

Context

The console — the approver's inbox (#95), run history, the hosted approval and notification side (ADR 0005) — needs a home that is always on, and the criterion it must meet is the lock one: two applies on one module never run at once, even across Worker restarts and resumed workflows. Issue #175 picks the platform. Whatever it is must also run OpenTofu, keep every apply behind a recorded approval, and never let a secret reach a log or a state file.

Decision

Cloudflare, one account, mapped piece by piece onto the ports the harness already declares:

Cloudflare pieceRoleHarness port
Worker + static assets, behind Cloudflare Accessthe console's HTTP surface; Access fronts the human side— (the venture's own entry point, ADR 0200)
D1fleet, runs, approvals, audit: the queryable halfRunLog/journal entries land here — the exportable audit
R2, S3-compatible, versioned, lockfile onstate backend: parked-run state and saved plans, addressed by digestStateStore
Containers, started on demand by a Durable Object, one pinned imagetofu, git, build-dist, wranglerIacTool (tofu), the build and script steps
Workflowsplan → waitForEvent(approval) → apply; the wait is the approval gatepark/resume durability (StateStore); ApprovalChannel and the Verdict's who+when
Durable Object per (account, module)the lock: one object serialises every run that would change that moduleRunLock (new, crates/core/src/ports/run_lock.rs) — leases with expiry and fencing tokens
Workers secrets / Secrets Storecredentials injected per run, never logged, never written to stateSecrets + the run's Redactor
Cron-triggered Workflow, nightlydrift: plan, compare, report — never apply unattendedthe same plan/approval shape, approval required before any apply
Owlpostapprover notificationsthe hosted ApprovalChannel side of ADR 0005

The lock deserves its own sentence: a Durable Object per (account, module) is single-threaded, so its check-and-grant is atomic by construction; the lease expires so a crashed run cannot wedge a module, and its fencing token climbs so a workflow that resumed after a takeover cannot apply anyway. The port carries that contract; this ADR only says which platform answers it.

Consequences

What this ADR does not yet deliver (follow-ups to #175): the Durable Object RunLock adapter, the plan → approval → apply Workflow, the bootstrap tooling and break-glass runbook as written procedures, and the test that proves a secret cannot reach a log. What exists today is the port contract the adapter must satisfy, its in-memory fake and the conformance kit in ks-testing.