ADR-0006: Hosting target is Cloudflare Pages + Workers + Neon + Upstash¶
- Status: Accepted
- Date: 2026-04-28
- Deciders: MASTER
- Related: #19, RFC-0001, #34, tech-stack, ADR-0007 (background jobs), ADR-0008 (search backend), ADR-0009 (frontend stack)
Context¶
RFC-0001 compared three hosting paths for the Next.js 16 web app + scraper workers: Cloudflare Pages + Workers (OpenNext adapter) + Neon + Upstash; Vercel + Neon + Upstash; Hetzner CX22 self-managed. bits.lb was deferred as "unknown."
The project is Lebanese-market-first, runs on evenings/weekends, has a tight cost ceiling, and already hosts its docs site on Cloudflare Pages. The personas research surfaces a mobile-heavy buyer cohort with spotty 4G — first-paint latency is non-trivial value. Cloudflare's Beirut PoP (BEY) gives 10–30 ms RTT vs ~80–120 ms via Frankfurt for the alternatives.
Decision¶
961tech ships on Cloudflare Pages + Workers (via the @opennextjs/cloudflare adapter), with Neon Postgres and Upstash Redis as the managed-service complements. DNS, TLS, and WAF stay under the existing Cloudflare account that already serves the docs site.
| Layer | Provider |
|---|---|
| Web app (Next.js 16) | Cloudflare Pages + Workers (OpenNext adapter) |
| Postgres | Neon (Frankfurt), via Cloudflare Hyperdrive for connection pooling |
| Redis | Upstash REST API |
| Background workers | Cloudflare Cron Triggers + scheduled Worker handler (per ADR-0007) |
| Docs site | Cloudflare Pages (already live, unchanged) |
| DNS / TLS / WAF | Cloudflare |
Consequences¶
Positive¶
- Beirut latency. RTT 10–30 ms vs 80–120 ms via Frankfurt for Vercel — material for a mobile-4G persona that pays attention to TTFB.
- Cost. ~$5/mo at M1 (Workers Paid + Neon free tier + Upstash free tier). Vercel Pro is $20/mo (Hobby disqualified — affiliate revenue is commercial).
- Account consolidation. Docs, app, DNS, TLS, WAF in one dashboard, one bill, one credential rotation surface.
- Workers Cron Triggers fit the 24–120 jobs/day workload that ADR-0007 sizes.
Negative¶
- OpenNext adapter lag. Bleeding-edge Next 16 features ship to Vercel first, OpenNext follows by weeks-to-months. New caching directives, PPR refinements, and image-optimization paths may need shims. Mitigation: Vercel stays as the documented fallback if a specific feature blocks us.
- No long-lived processes. No 24/7 BullMQ daemon. Workers are request-scoped; Cron Triggers run on schedule. Scrapers must complete inside Worker invocation budgets (30s CPU on paid; subrequest limits govern wall-clock). ADR-0007's pg-boss + scheduled-drain pattern is right-shaped for this; streaming pipelines would not be.
ioredisis gone. Workers don't support raw TCP forioredisreliably — Upstash REST is the path. Drops cleanly with the ADR-0007 decision (we no longer need Redis for queue use anyway).- Higher lock-in than Vercel. Worker bindings (
env.X), KV usage, OpenNext build artefacts mean migrating away requires re-testing against vanilla Node. DB egress is trivially small.
Neutral¶
- Workers runtime quirks.
nodejs_compatflag opt-in; some Node APIs need shims;next/cachesemantics on KV/R2 have eventual-consistency nuances. Documented in deployment guide (TBD). - Migration cost from-Cloudflare-to-Vercel later is bounded but real. We commit deliberately; the cost would only be paid if a feature genuinely blocks.
Alternatives considered¶
Vercel + Neon + Upstash — rejected¶
First-class Next.js, every new feature day-one, Fluid Compute (long-lived functions). Loses on: cost (Pro tier \(20/mo vs ~\)5), Beirut latency (~80–120 ms via FRA/CDG/LHR), and account fragmentation. Wins reserved for a future revisit if a specific Next 16 feature blocks on OpenNext.
Hetzner CX22 self-managed — rejected¶
Single VPS, Docker compose, full Node runtime. Wins on: lock-in (none), cost (~$6/mo). Loses on: ops burden (2–4 hrs/mo steady-state, more after kernel CVEs). Ops appetite was the deciding factor — the project is evenings-and-weekends; weekend-on-call is friction we don't want.
bits.lb beta — deferred¶
Lebanese-host-first option. Documented in RFC-0001 as "treated as unknown" pending a separate research pass. Revisit if a marketing / principles requirement surfaces; not a blocker today.
Open items pulled forward to follow-up tickets¶
- Concrete deployment.md — promote
docs/architecture/deployment.mdfromstubtoactive. Filed as part of #19 implementation. - Env-var rotation —
IP_HASH_SECRETliteral default to a real ≥32-byte random value (per tech-stack audit). Lands in #19's implementation PR. - ci.yml — app CI gate (
vitest run,tsc --noEmit,eslint,npm audit) currently absent. Filed as a separate code ticket alongside #19. - Observability — pulled along into #43 and tracked there.