From 97566a82834a75f9ca37ebd5fa94dfd4386150af Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 23:01:29 +0000 Subject: [PATCH] Hosting research + ADR 0011 (Proposed): Cloudflare Workers, DB finalized SQLite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Where does the airship dock? Four candidates costed at three scale tiers (research/2026-07-30-hosting-infra.md, with sources): - Cloudflare Workers + for SaaS: ~$5 / ~$10 / ~$150 per month — custom hostnames ($0.10/mo after 100 free) are the only real scale cost, and they track paying users at ~2% of revenue - Hetzner VPS + Caddy on-demand TLS: cheapest at every tier, keeps literal SQLite+Litestream, wisp.place recipe proven — but makes a nights-and-weekends solo dev the 24/7 ops team (PD-7) - Vercel: $20/mo floor before the first paying user, wildcard requires surrendering mooring.page DNS, worst scale curve - Fly.io: dominated on cost and reliability; LiteFS sunset ADR 0011 (Proposed, awaiting ratification): Cloudflare Workers as hosted target; D1 finalizes ADR 0010's provisional SQLite (file + Litestream in self-host mode); VPS+Caddy documented as first-class self-host path and escape hatch; wisp.place-style DID-in-TXT domain verification; one named spike (oauth-client-node under nodejs_compat, @atcute fallback) before scaffolding commits. Also queued in NEXT: item 2 is now "ratify ADR 0011". Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014mXNfP5m9NhhjVgM5VVJ5E --- docs/FEASIBILITY.md | 2 +- docs/NEXT.md | 7 ++- .../adr/0011-hosting-cloudflare-workers.md | 27 ++++++++ docs/research/2026-07-30-hosting-infra.md | 61 +++++++++++++++++++ docs/research/README.md | 1 + 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 docs/decisions/adr/0011-hosting-cloudflare-workers.md create mode 100644 docs/research/2026-07-30-hosting-infra.md diff --git a/docs/FEASIBILITY.md b/docs/FEASIBILITY.md index ed725c1..bac4749 100644 --- a/docs/FEASIBILITY.md +++ b/docs/FEASIBILITY.md @@ -83,4 +83,4 @@ Deferred to v2+: teal.fm, writing third-party lexicons, the pluggable translatio - ~~Product name~~ → decided: **Mooring** at mooring.page (PD-8; research in [`research/2026-07-30-naming.md`](research/2026-07-30-naming.md)). Trademark screen passed (2026-07-30; note appended to the naming file). Fahrenheit stays as codename. - ~~Our lexicon namespace NSID~~ → decided: `page.mooring.*` (ADR 0009). - ~~Tech stack~~ → decided: TypeScript + SvelteKit, first-party `@atproto/*`, direct PDS reads for v1 (ADR 0010; research in [`research/2026-07-30-language-ecosystems.md`](research/2026-07-30-language-ecosystems.md), [`-appview-infra.md`](research/2026-07-30-appview-infra.md), [`-comparable-stacks.md`](research/2026-07-30-comparable-stacks.md)). -- Hosted infrastructure target and cost model (DB choice finalizes here too — ADR 0010 leaves it provisional). +- ~~Hosted infrastructure target and cost model~~ → ADR 0011 **Proposed** (2026-07-30, awaiting ratification): Cloudflare Workers + Cloudflare for SaaS; DB finalized SQLite-family (D1 hosted / file self-host); research in [`research/2026-07-30-hosting-infra.md`](research/2026-07-30-hosting-infra.md). diff --git a/docs/NEXT.md b/docs/NEXT.md index 40cec26..e481eda 100644 --- a/docs/NEXT.md +++ b/docs/NEXT.md @@ -2,7 +2,7 @@ The flight plan. Each item carries enough context to start cold; update this file whenever an item lands (move it to "Done") or a new one is queued. Decisions made while working an item still go through `decisions/` as usual. -_Last updated: 2026-07-30 (trademark screen + domain ops landed)._ +_Last updated: 2026-07-30 (hosting research + ADR 0011 proposed; TMview check recorded)._ ## Where things stand @@ -14,9 +14,9 @@ Feasibility is done and the verdict was **build it** (see `FEASIBILITY.md`). All Design the record types for site config + authored pages + source bindings (ADR 0009 sketches `page.mooring.site` / `.page` / `.source` as starting points — not binding). Before shipping v1 records: review hard (schemas are effectively immutable once records exist in the wild — ADR 0004), check lexicon.community for prior art, publish on-network via `com.atproto.lexicon.schema`, and publish permission sets for our OAuth scopes (ADR 0007). Study `site.standard.*` and `id.sifa.*` for conventions (both linked in the lexicon research file). -### 2. Hosted infrastructure target + cost model +### 2. Ratify ADR 0011 (hosting: Cloudflare Workers + for SaaS; DB: SQLite/D1) -Where does the hosted multi-tenant service (ADR 0005) run, and what does a free-tier user cost us? Must support custom domains with per-tenant TLS at ~$6–8/mo price points (PD-4). This decision also finalizes the DB (ADR 0010 left it provisional-SQLite; a host that favors Postgres flips it — ADR 0010 refers to this as "NEXT item 5", its number at the time). Starting points from the stack survey (`research/2026-07-30-comparable-stacks.md`): the three documented custom-domain patterns — wisp.place's Caddy on-demand-TLS + DNS-TXT-verification recipe (most transferable to self-managed infra), Leaflet's Vercel domains API, Blento's CNAME-to-apex on Cloudflare Workers. SvelteKit (ADR 0010) has first-class adapters for all three targets. +The research is done (`research/2026-07-30-hosting-infra.md`: Cloudflare vs Hetzner-VPS+Caddy vs Vercel vs Fly, costed at three scale tiers) and ADR 0011 is written as **Proposed**: Cloudflare Workers hosted target, D1 finalizing ADR 0010's provisional SQLite, VPS+Caddy as the documented self-host path, DID-in-TXT domain verification. Jacob reads, pushes back or ratifies (flip Status to Accepted). One named spike rides along: verify `@atproto/oauth-client-node` under Workers `nodejs_compat` before scaffolding commits (fallback: `@atcute`, already approved). (This was "hosted infrastructure target + cost model" — ADR 0010 calls it "NEXT item 5", its number at the time.) ### 3. Build v1 (per ADR 0008 — the scope is ratified; don't re-scope) @@ -31,6 +31,7 @@ OAuth login → read-only adapters for Bluesky, standard.site (+ `pub.leaflet.*` ## Done +- 2026-07-30 — Hosting research done, ADR 0011 **Proposed** (awaiting ratification): Cloudflare Workers + Cloudflare for SaaS as hosted target (~$5/mo launch, ~2%-of-revenue custom-domain COGS at scale); DB finalized SQLite-family (D1 hosted / file+Litestream self-host); VPS+Caddy (wisp.place recipe) as first-class self-host path; `_mooring. TXT did:…` verification. Fly.io ruled dominated; Vercel ruled out ($20 floor, DNS coupling, worst scale curve). Research: `research/2026-07-30-hosting-infra.md`. - 2026-07-30 — Trademark screen on "Mooring": **passed**. US side clean — the only live software-class mark is "ONLINE MOORING" (boat-mooring ASP for harbormasters, different services); bare MOORING marks all dead. EU screened via secondary sources, then confirmed same day by Jacob's manual TMview check (classes 9/35/38/42 — clear); no residuals. Detail appended to `research/2026-07-30-naming.md`. Brand spend fully unblocked. - 2026-07-30 — Domain ops: auto-renew + registrar lock confirmed on **mooring.page** (Jacob). Defensive adjacent registrations remain optional/unpursued. - 2026-07-30 — Tech stack decided: ADR 0010 (TypeScript + SvelteKit core, first-party `@atproto/*`, direct PDS reads for v1 — no quickslice/HappyView, Tap named v2 liveness candidate, BEAM benched for v2 services). Research checkpoints: language ecosystems, AppView infra, comparable stacks. diff --git a/docs/decisions/adr/0011-hosting-cloudflare-workers.md b/docs/decisions/adr/0011-hosting-cloudflare-workers.md new file mode 100644 index 0000000..4fa3845 --- /dev/null +++ b/docs/decisions/adr/0011-hosting-cloudflare-workers.md @@ -0,0 +1,27 @@ +# ADR 0011: Hosted target — Cloudflare Workers + Cloudflare for SaaS; DB finalized as SQLite-family (D1 hosted / file self-host) + +**Status:** Proposed · 2026-07-30 (awaiting Jacob's ratification) + +## Context + +ADR 0005 committed us to a hosted multi-tenant service as the paid path; PD-4 fixed pricing at free subdomain / ~$6–8/mo custom domain; ADR 0010 chose SvelteKit and left the database "provisional SQLite-family," with the final call landing here. The service must do per-tenant TLS on customer custom domains cheaply enough that a paying user's infra cost is a small fraction of $6–8/mo, and free subdomain users must cost ~nothing. PD-7 (solo, nights-and-weekends) makes operational burden a first-class cost, not a footnote. + +Research (2026-07-30, `docs/research/2026-07-30-hosting-infra.md`, with sources): four candidates costed at three scale tiers — (a) 110 users, (b) 1,100, (c) 11,000 (10:1 free:paid). Cloudflare ~$5/$10/$150 per month; Hetzner VPS + Caddy ~$7/$11/$20–45; Vercel ~$20/$25/$400–900; Fly.io ~$18/$59/$400. Vercel's floor is $20/mo before the first paying user (Hobby prohibits commercial use), its wildcard requires surrendering mooring.page DNS to its nameservers, and its scale curve is bandwidth-hostage to free-tier growth. Fly is dominated: pricier than the VPS at every tier, per-hostname cert fees taxing exactly what we charge for, and a 2025–26 reliability record (~13 incidents/month, no SLA) incompatible with selling uptime. The real choice was Cloudflare vs VPS. + +## Decision + +1. **Hosted target: Cloudflare Workers** (SvelteKit via `@sveltejs/adapter-cloudflare`, Workers Paid $5/mo), with **Cloudflare for SaaS custom hostnames** for paid custom domains (100 free, then $0.10/hostname/mo — ~2% of a $7/mo subscription, and it scales only with *paying* users; free-tier users cost ~$0: static assets and wildcard `*.mooring.page` TLS are free, egress is unmetered). +2. **Database finalized: SQLite-family, confirming ADR 0010 §5** — concretely **D1** in the hosted service (included in the $5 plan; 10GB/db cap is ample for config/cache/billing per ADR 0004) and a **plain SQLite file (+ Litestream backup) in self-host mode**. Both sit behind the swappable data layer from ADR 0010 (Drizzle/Kysely speak both dialects). Hyperdrive + external Postgres is the named escape hatch if D1 semantics ever chafe; adopting it would supersede this point, not this ADR. +3. **Self-host path: the VPS + Caddy on-demand TLS recipe** (wisp.place pattern — Caddyfile with `ask` endpoint, wildcard + catch-all blocks, `adapter-node`), documented as a first-class deployment mode per ADR 0005. It is also our own escape hatch: at tier (c) it's the cheaper option (~$20–45 vs ~$150/mo), and we accept paying the managed premium until/unless scale makes the ops trade worth revisiting. +4. **Custom-domain verification: DNS TXT carrying the user's DID** (`_mooring. TXT did:plc:…`), adopted from wisp.place — ownership ties to the atproto identity, not an app account. Used identically in hosted (gates the custom-hostname API call) and self-host (gates Caddy's `ask`). +5. **Apex-domain policy:** custom hostnames are CNAME-based; apex works only where the customer's DNS host supports CNAME-flattening/ALIAS. Onboarding copy recommends (in order): domain's DNS on Cloudflare free / a flattening-capable host (Namecheap, Porkbun…), or serve on `www` with an apex redirect. Same answer as Blento's; Enterprise Apex Proxying is not on the menu. +6. **Named spike before scaffolding commits:** verify `@atproto/oauth-client-node` runs on Workers under `nodejs_compat`. If it doesn't, use the `@atcute` OAuth clients (already approved in ADR 0010; proven on Workers by Blento). The spike's outcome does not reopen this ADR either way. + +## Consequences + +- Zero server ops for a nights-and-weekends solo dev (PD-7): no OS updates, no pager, scale-to-zero. Estimated infra: ~$5/mo at launch, ~$150/mo at 1,000 paying users (~2% of revenue), dominated by per-hostname fees that track revenue rather than free-tier growth. +- Workers constraints accepted: 10MB gzipped bundle, no `fs`, `nodejs_compat` quirks — Blento (SvelteKit + atproto on Workers, our nearest product shape) is the existence proof this fits. +- Vendor concentration (DNS zone, compute, DB, TLS all Cloudflare) is mitigated by the ADR 0005 separable renderer, the swappable data layer, and point 3's documented exit: the self-host recipe *is* the migration plan, tested continuously by the community using it. +- ADR 0010's provisional DB note is resolved; no Postgres, no new moving parts. Large media should be served from users' PDS blobs (or R2 later), keeping us clear of Cloudflare's CDN media terms and Workers CPU. +- mooring.page's zone moves to (free) Cloudflare DNS — unlike the Vercel option this carries no plan cost and doesn't constrain other records. +- Revisit triggers: the OAuth-on-Workers spike failing *and* @atcute proving insufficient (would reopen hosting, not just the client); custom-hostname pricing changes; sustained tier-(c) scale making the ~$100/mo VPS delta meaningful against then-current ops appetite. diff --git a/docs/research/2026-07-30-hosting-infra.md b/docs/research/2026-07-30-hosting-infra.md new file mode 100644 index 0000000..43a365d --- /dev/null +++ b/docs/research/2026-07-30-hosting-infra.md @@ -0,0 +1,61 @@ +# Research: hosted infrastructure target + cost model + +**Checkpoint date:** 2026-07-30. Input to ADR 0011 (NEXT item "hosted infrastructure target + cost model"; ADR 0010 called it "NEXT item 5"). Method: three parallel research agents (Cloudflare / Vercel / VPS+Caddy & Fly.io), official pricing pages preferred, secondary sources flagged inline. Question: where does the hosted multi-tenant service (ADR 0005) run, what does a free-tier user cost, what does a paid custom-domain user cost against ~$6–8/mo (PD-4), and which way does the provisional-SQLite call (ADR 0010 §5) fall? + +**Model assumptions used throughout:** ~1,000 page views/user/month (personal sites are low-traffic); SSR with healthy caching; paid users need 1–2 custom hostnames each (apex + www). Three scale tiers: (a) 100 free + 10 paid, (b) 1,000 free + 100 paid, (c) 10,000 free + 1,000 paid. Revenue at $7/mo midpoint: (a) $70, (b) $700, (c) $7,000/mo. + +## The headline table + +| Monthly infra cost | (a) 110 users | (b) 1,100 users | (c) 11,000 users | Cost driver at scale | DB story | +|---|---|---|---|---|---| +| **Cloudflare Workers + for SaaS** | **~$5** | **~$10** | **~$150** | Custom hostnames ($0.10/mo each after 100 free) — scales only with *paying* users, ~2% of revenue | D1 (SQLite semantics) in the $5 plan | +| **VPS (Hetzner EU) + Caddy** | ~$7 | ~$11 | ~$20–45 | The box itself; egress inside 20TB allowance | SQLite file + Litestream→R2 (~$0) | +| **Vercel Pro** | ~$20 | ~$20–25 | ~$400–900 (mid ~$530) | Fast Data Transfer $0.15/GB past 1TB + edge requests | Neon Postgres (file-SQLite non-viable) | +| **Fly.io (2 machines)** | ~$18 | ~$59 | ~$375–425 | Certs $0.10/mo × every paid domain + egress $20/TB | Volume SQLite + Litestream (LiteFS sunset) | + +Free-tier users are nearly free everywhere except Vercel/Fly at scale: wildcard `*.mooring.page` TLS costs $0 on Cloudflare (Universal SSL covers first-level wildcards) and ~$0 on a VPS (one DNS-challenge wildcard cert); a free user's marginal cost is fractions of a cent of compute. **The paid tier is where hosts differ**: per-domain TLS is $0.10–0.20/user/mo on Cloudflare, ~$0 marginal on VPS/Caddy, $0 on Vercel, $0.10–0.20 on Fly. + +## Option 1 — Cloudflare Workers + Cloudflare for SaaS + +- **Custom hostnames (per-tenant TLS):** bundled on all plans incl. Free; **first 100 free, then $0.10/hostname/mo**, pay-as-you-go cap 50,000 (raised from 5,000 in [May 2025](https://developers.cloudflare.com/changelog/2025-05-19-paygo-updates/)). Customer CNAMEs to our zone; Cloudflare validates (TXT / HTTP auto), issues, renews, routes to a fallback origin. ([plans](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/plans/), [getting started](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/start/getting-started/)) +- **The apex gotcha:** A-record apex pointing is unsupported below Enterprise (Apex Proxying/BYOIP are Enterprise add-ons). Non-Enterprise answer: customer's DNS host must do CNAME-flattening/ALIAS at apex (Cloudflare DNS, Namecheap, Porkbun…do), else "use www + redirect" or "move DNS to free Cloudflare." **Blento hit the same wall and their docs just tell users to put DNS on Cloudflare** ([their CustomDomain.md](https://github.com/flo-bit/blento/blob/main/docs/CustomDomain.md)). No wildcard custom hostnames below Enterprise → apex+www = 2 × $0.10. +- **Workers:** Paid $5/mo incl. 10M requests + 30M CPU-ms; **static asset requests free and unlimited** — only SSR invocations bill; no egress charges at all ([pricing](https://developers.cloudflare.com/workers/platform/pricing/), [static assets billing](https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/)). Limits: 10MB gzipped bundle (Paid), 128MB memory, no `fs`, `nodejs_compat` flag for Node APIs ([limits](https://developers.cloudflare.com/workers/platform/limits/)). +- **SvelteKit:** `@sveltejs/adapter-cloudflare` is the official default-family adapter, full SSR ([docs](https://svelte.dev/docs/kit/adapter-cloudflare)). **Blento is the existence proof on our exact product shape** (SvelteKit + atproto on Workers). +- **DB:** D1 (SQLite semantics) inside Workers Paid: 25B rows read, 50M written, 5GB incl.; 10GB/database cap; Time Travel 30-day restore ([pricing](https://developers.cloudflare.com/d1/platform/pricing/), [limits](https://developers.cloudflare.com/d1/platform/limits/)). Cron Triggers free (draw normal allowance, 15 min CPU for jobs). Hyperdrive (free, incl.) + external Postgres is the escape hatch. Durable Objects available if ever needed. +- **Cost detail (c):** 16.5M billed reqs → $1.95 over; 165M CPU-ms → $2.70 over; D1 ≪ included; 1,400 hostnames over free → **$140**; base $5 ⇒ **~$150/mo**. Extremely insensitive to compute; hostnames dominate and track revenue (~2%). +- **AGPL/ToS:** no conflict; the scary HTML-serving clause (§2.8) was removed May 2023; residual: serve large media from R2/PDS blobs, not the Worker ([service-specific terms](https://www.cloudflare.com/service-specific-terms-application-services/)). + +## Option 2 — Self-managed VPS + Caddy on-demand TLS (the wisp.place pattern) + +- **Caddy on-demand TLS:** cert obtained during the first TLS handshake for an unknown SNI, gated by a mandatory `ask` endpoint (our ~10-line route checking the verified-domains table); LE with ZeroSSL auto-fallback; maintainer-vouched at **500k+ domains** ("specially designed for exactly that use case" — [caddy.community](https://caddy.community/t/scaling-caddy-to-hundreds-of-thousands-of-domains/16508)); Apache-2.0, no commercial license needed ([docs](https://caddyserver.com/docs/automatic-https), [on-demand page](https://caddyserver.com/on-demand-tls)). LE limits post-2025 scaling are a non-issue at our size ([rate limits](https://letsencrypt.org/docs/rate-limits/)). Wildcard `*.mooring.page` via DNS challenge coexists with on-demand for customer domains. +- **The wisp.place recipe** ([docs.wisp.place/deployment](https://docs.wisp.place/deployment)) — closest architectural cousin in the Atmosphere, MIT: Caddyfile with `on_demand_tls { ask … }`, wildcard blocks, catch-all `https://` block; **domain ownership verified via `_wisp. TXT did:plc:…`** — the TXT record carries the user's atproto DID, tying the domain to the PDS identity rather than an app account. Steal this verbatim (as `_mooring.`). +- **Pricing (July 2026 — Hetzner repriced twice this year,** ~30–37% April on everyone, then June +113–175% on CPX/CCX lines for new orders; DRAM shortage; [byteiota](https://byteiota.com/hetzner-june-2026-price-shock/), [northflank](https://northflank.com/blog/hetzner-cloud-server-price-increases)**):** CX23 2vCPU/4GB **€5.49** (EU-only), CX33 ~€8.5–9 (unverified extrapolation), CX43 ~€16.5; +€0.60 IPv4; **20TB egress included EU** (US regions: 1TB incl., but US regions only carry the now-expensive CPX line — **the cheap Hetzner story is EU-only**, ~100ms RTT for US visitors unless fronted by free Cloudflare). Alternatives: Netcup (~€4–8.4, has a Virginia location), DigitalOcean ($24/mo 2vCPU/4GB, 4TB egress, USD billing). +- **Ops burden, honestly:** OS updates, deploys, backups, monitoring, and the 2am pager are ours — est. **1–3 hrs/month steady-state** plus incident tail risk. PaaS layers (Coolify Apache-2.0 ~59k★, Dokku, Kamal) soften deploys; Caddy must still own 80/443 (Coolify defaults to Traefik — swap is custom config). SPOF math is forgiving *for data* (canonical data is in users' PDSes; Litestream restore ≈ minutes) but downtime is on us. **Back up Caddy's cert storage** or a rebuild collides with LE rate limits. +- **SQLite survives here unchanged:** the box runs a plain SQLite file + **Litestream v0.5.x** (LTX format, PITR; actively maintained, Fly's own blog announced it — [fly.io blog](https://fly.io/blog/litestream-v050-is-here/)) replicating to **R2 (free tier ≈ $0/mo)** or B2. + +## Option 3 — Vercel ("Vercel for Platforms") + +- **Multi-tenant domains:** first-class product; **$0 per domain, unlimited on Pro** (soft cap 100k/project); add via REST/SDK, auto-TLS per domain; apex via A record to an assigned anycast IP (read it from the API — the classic 76.76.21.21 is no longer universal). **Leaflet is the atproto precedent** ("dead simple… routing logic directly in our code base" — [their stack post](https://lab.leaflet.pub/3lrvqjio6e22d)). ([platforms docs](https://vercel.com/docs/platforms), [limits](https://vercel.com/docs/platforms/multi-tenant-platforms/limits)) +- **Wildcard `*.mooring.page`:** all plans, **but requires delegating mooring.page's nameservers to Vercel** — couples our DNS (MX, `_atproto` TXTs, everything) to the hosting vendor. +- **Pricing:** Hobby prohibits commercial use → **Pro $20/seat/mo is the floor from day one**; then usage: Fast Data Transfer 1TB incl. then **$0.15/GB**, edge requests 10M incl. then $2/M; Fluid compute makes SSR cheap; $20/mo usage credit ([pricing](https://vercel.com/pricing), [fair use](https://vercel.com/docs/limits/fair-use-guidelines)). +- **DB:** Vercel Postgres is gone (sunset 2025 → **Neon** via marketplace); **file-SQLite confirmed non-viable** (ephemeral `/tmp` per instance); Turso is the SQLite-flavored workaround. Choosing Vercel flips ADR 0010's provisional SQLite to Postgres. ([Neon transition guide](https://neon.com/docs/guides/vercel-postgres-transition-guide)) +- **SvelteKit:** `@sveltejs/adapter-vercel` first-party, active (v7-next July 2026); per-route ISR is the cost lever; Vercel's middleware product doesn't work with SvelteKit (Host-routing in `hooks.server.ts` — fine). +- **Cost (c):** ~3.3TB FDT → ~$345 + ~88M edge reqs → ~$156 + small compute/ISR/DB ⇒ **mid ~$530, range $400–900/mo** — 6–13% of revenue, driven by *free-tier* bandwidth. Watch: routing PDS avatars through `/_vercel/image` adds a real line item; serve pre-sized from PDS blob CDN. + +## Option 4 — Fly.io + +- **Certs $0.10/mo per hostname after 10 free** (wildcard $1/mo) — a tax on exactly what we charge for: **$99/mo at 1,000 paid domains**. Egress **$0.02/GB** ≈ 20× Hetzner's overage. Machines cheap-ish (shared-1x/1GB $5.92); volumes $0.15/GB; **no free tier for new customers**. ([pricing](https://fly.io/docs/about/pricing/)) +- **Reliability 2025–26 still poor:** ~13 incidents/month average, May 2026 had incident-days on 20 of 31, no SLA on standard plans, recurring Consul/Corrosion root causes ([kuberns analysis](https://kuberns.com/blogs/is-fly-io-good-for-production/)). Wrong platform for a product whose pitch includes "your site is always up." +- **LiteFS effectively sunset** (LiteFS Cloud shut down Oct 2024; effort redirected to Litestream). The 2026 Fly recipe is machine+volume+Litestream — identical to the VPS recipe minus control of the box. +- **Verdict: dominated.** Costs more than the VPS at every tier with worse reliability; costs more than Cloudflare at every tier with more ops. Useful only as a documented escape hatch from a VPS (ports in a weekend). + +## Cross-cutting notes + +- **`@atproto/oauth-client-node` on Workers is unverified.** It's Node-oriented; Workers' `nodejs_compat` has grown a lot, but this needs a spike before committing code. Fallback exists and is already approved in ADR 0010: the `@atcute` OAuth clients — which is what Blento uses on Workers today. This is the main technical unknown of the Cloudflare option. +- **Self-host alignment (ADR 0005):** the VPS+Caddy recipe *is* the self-host deployment story we owe the community regardless of which hosted target we pick — wisp.place's docs prove it fits in a page. Choosing Cloudflare for hosted doesn't lose this; it just means the hosted path and self-host path differ (adapter-cloudflare/D1 vs adapter-node/SQLite-file), which the swappable data layer (ADR 0010 §5, Drizzle/Kysely both speak D1 and better-sqlite3) and separable renderer (ADR 0005) are designed to absorb. +- **Egress asymmetry is the quiet decider at scale:** Cloudflare charges $0 egress, Hetzner EU includes 20TB, Vercel $0.15/GB past 1TB, Fly $0.02/GB. Personal sites are bandwidth-shaped (HTML + images), so this asymmetry, not compute, separates the curves. +- **Currency/billing:** Hetzner bills EUR from Germany (conversion fees on US cards); Cloudflare/Vercel/Fly bill USD. + +## So what + +Two real candidates. **Cloudflare Workers + for SaaS** is the near-zero-ops managed option whose cost curve is the best of the managed trio at every tier (~$5 → ~$150/mo) and whose dominant scale cost (custom hostnames) tracks *revenue* at ~2%, not free-tier growth; Blento proves SvelteKit + atproto on this exact stack, and D1 keeps ADR 0010's SQLite-family call intact. **Hetzner VPS + Caddy** is cheapest at every tier (~$7 → ~$20–45), keeps literal SQLite+Litestream, and doubles as the self-host recipe — but makes a nights-and-weekends solo dev (PD-7) the 24/7 ops team, and Hetzner's 2026 double-repricing plus EU-only cheap tier dent the "boring and predictable" argument. Vercel's floor ($20 before the first paying user), DNS coupling, and worst-in-class scale curve rule it out despite the slickest domain API; Fly is dominated outright. The cost difference between the two finalists at realistic scale (~$100–130/mo at 1,000 paying users ≈ $7k MRR) is noise; the ops difference is not. Recommendation carried into ADR 0011: **Cloudflare as the hosted target, VPS+Caddy documented as the self-host path and escape hatch**, with one named spike (OAuth client on Workers) before scaffolding commits to it. diff --git a/docs/research/README.md b/docs/research/README.md index dd4468b..0c82d34 100644 --- a/docs/research/README.md +++ b/docs/research/README.md @@ -11,5 +11,6 @@ Findings from research sessions, preserved so future sessions (human or agent) d | [`2026-07-30-language-ecosystems.md`](2026-07-30-language-ecosystems.md) | 2026-07-30 | atproto library maturity by language (TS/Elixir/Gleam/…); OAuth client state | | [`2026-07-30-appview-infra.md`](2026-07-30-appview-infra.md) | 2026-07-30 | quickslice vs HappyView vs Tap vs Microcosm; do we need an AppView? | | [`2026-07-30-comparable-stacks.md`](2026-07-30-comparable-stacks.md) | 2026-07-30 | What 8 comparable atproto apps run on; custom-domain serving patterns | +| [`2026-07-30-hosting-infra.md`](2026-07-30-hosting-infra.md) | 2026-07-30 | Hosted infra target + cost model: Cloudflare vs VPS+Caddy vs Vercel vs Fly | Conventions: one dated file per topic, source URLs inline, and an honest "so what" at the end of each. When you re-research a topic, add a new dated file rather than editing the old one, and update this index. -- 2.51.2