Skip to main content

ADR-0001 — Cloudflare stack as the runtime

  • Status: Accepted
  • Date: 2026-04-15
  • Decision-makers: Tom Anderson
  • Supersedes: —
  • Superseded by: —

Context

Helm needs a runtime that can host:

  • One isolated app instance per shop
  • A SQL database per shop with low operational overhead
  • Object storage per shop for receipts/photos
  • Scheduled jobs per shop
  • A static documentation site (this bible)
  • A static public marketing site per shop

Constraints:

  • One developer for the foreseeable future. Ops surface must be small.
  • Per-shop blast radius — a problem at one shop must not affect another.
  • POS UX matters; cold-start latency budget is < 100ms.
  • Shops are in Canada (BC start), so a Canadian PoP is preferable.

Alternatives evaluated:

  • AWS (Lambda + RDS + S3 + CloudFront + EventBridge) — battle-tested but per-shop infra is heavy: VPCs, security groups, RDS instances, IAM roles. Ops surface vastly larger than a one-person team can sustain at 50 shops. Cold-start is acceptable but worse than Workers.
  • Vercel (Functions + Vercel Postgres + Blob + Cron) — Functions cold-start hurts POS UX. Postgres is multi-tenant by default; per-shop Postgres requires per-shop projects, multiplying ops. Not a great fit for the single-tenant model.
  • Self-hosted (VPS or k8s + Postgres + MinIO + scheduled jobs) — operationally untenable for one person responsible for shop SLAs.
  • Cloudflare (Workers + D1 + R2 + Pages + Cron) — single platform; per-shop deploys are first-class; Workers cold-start is sub-10ms; D1 is single-tenant SQLite that's easy to back up and reason about; R2 is S3-compatible with no egress fees; global PoPs include Toronto and Vancouver.

Decision

Use Cloudflare Workers + D1 + R2 + Pages + Cron as the entire runtime. No mix-and-match with another cloud. All infrastructure is Cloudflare; deploys are via wrangler.

Consequences

Positive:

  • One vendor, one CLI, one billing, one set of docs to learn.
  • Per-shop isolation is the platform's natural unit (one Worker = one deploy = one shop).
  • Sub-10ms cold start makes the POS responsive.
  • D1's SQLite-based model means schema, backups, and exports are simple files.
  • R2's no-egress-fees pricing is a meaningful saving at scale.
  • Cloudflare Access on Pages gives us SSO for the bible without extra infra.

Negative:

  • Vendor lock-in. Migration off Cloudflare would be a large project. Mitigation: the schema is plain SQLite (portable), the JS is plain JS (portable), the bindings are the lock-in surface and they're a small number of well-documented APIs.
  • D1 is younger than Postgres; some advanced features (full-text search, partial indexes) lag.
  • Workers' CPU-time limit (50ms standard, configurable up) means heavy computation needs to be moved out of the request path.
  • A Cloudflare-wide outage takes all shops down simultaneously — see single-tenant per shop for what isolation we still get and what we don't.

Notes

This ADR is foundational; most other ADRs build on it. Revisit if Cloudflare changes pricing structure significantly or if D1 adds limits incompatible with our scale.

See also