Repository navigation
Plan: move the site to Cloudflare Workers with Effect v4 and Alchemy #70
Copy link
Copy link
Open
Description
Plan for moving the site off Next.js and Vercel onto Cloudflare Workers, built with Effect v4 and deployed with Alchemy. It's a proposal to agree on before the first PR; the open questions are at the end.
Goals
- Fast: under 70 ms full page loads for public pages, measured and enforced in CI rather than hoped for.
- Effect v4 end to end: typed errors, services and Schema-validated data from the database to the HTML.
- Everything declared in Alchemy: Workers, R2, Hyperdrive, Queues, cron, DNS and PR previews, all deployed from CI.
- Deterministic: generated artifacts are reproducible, contracts are checked, and migrations run in CI, not by hand.
- Tiny: no React and no framework runtime on public pages. They ship HTML, CSS and only the JavaScript a page needs.
Starting point
- Already on Cloudflare:
infra/(Alchemy v2 on Effect 4.0, state in Cloudflare) and media in R2 onmedia.allthings.dev, written through an upload Worker (Declare the media R2 bucket with Alchemy #64–Copy images still on S3 into R2 during the hourly sync #68). - Still on Next 16 on Vercel: 21 pages and 27 route handlers.
- Public pages: home, event pages, speakers, about, code of conduct.
- Feeds: RSS, sitemap, robots.
- OG, preview and QR images.
- The public API (
/api/v1/events,/api/v1/speakers) and MCP (/mcp), with the CLI built on that contract. - The admin area, Stack Auth sign-in and profiles.
- The hourly Luma sync with cover and photo ingestion.
- Data: Neon Postgres stays the source of truth.
Architecture
| Concern | Choice | Why |
|---|---|---|
| Runtime | One Effect-native Worker (Cloudflare.Worker in Alchemy) for every route, plus cron triggers and Queue consumers |
One deployable unit; bindings instead of credentials |
| Rendering | Server-rendered HTML from typed JSX compiled to strings (@kitajs/html), with escaping by default |
Typed, XSS-safe templates with zero client runtime; components stay plain functions |
| Interactivity | Small vanilla TypeScript islands (Web Components) only where needed: mobile nav, photo mosaic, admin forms | Public pages load little or no JavaScript |
| Speed | Public pages are pure functions of data. They're rendered when data changes and served from Cloudflare's cache. Each change bumps a data version, and pages are cached by version, so stale HTML can't outlive a change | Cache hits are answered at the edge, the only way to reliably hit <70 ms |
| Data access | @effect/sql-pg over Hyperdrive to Neon, with Effect Schema domain models |
Pooled, low-latency Postgres from Workers, and validation at the boundary |
| Migrations | Effect SQL migrator files, run by CI against a Neon branch per PR and against production on deploy | Ends hand-applied migrations |
| Images | R2 plus Cloudflare Images transformations, with AVIF/WebP variants at deterministic keys made at ingest time by a Queue consumer | Replaces sharp and next/image |
| OG / preview / QR | Rendered at ingest or on data change with satori + resvg-wasm, then stored in R2 | Deterministic, cacheable, never rendered on a request |
| Public API + MCP | Effect HttpApi with Schema, which yields OpenAPI and the MCP tools from one definition |
One contract for the site, the CLI and agents |
| Sync | A Workers cron trigger runs the Luma sync; ingestion runs through Queues with retries | No 60-second function limit, retries built in |
| Admin auth | Cloudflare Access in front of /admin (GitHub or email OTP for Erik and Andre). Stack Auth stays only if member profiles still need it |
No auth code to maintain for two admins |
| Observability | Workers Logs plus Effect tracing exported over OTLP | Traces cover the whole request |
| Previews | One Alchemy stage per PR (pr-<n> Workers and preview buckets), torn down when the PR closes |
Replaces Vercel previews, and the PR header links to them |
Repository shape:
core/: Effect services, Schema models, data access and tests, shared by the Worker and the CLI.web/: the Worker, with routes, templates and islands.infra/: grows to declare everything above.
Migration: build alongside, then cut over
- Core.
core/with Schema models and data access for events, talks, speakers, hosts and images, with contract tests against today's API responses. - Read-only public site on
next.allthings.dev: home, event pages, speakers, about, feeds, sitemap, robots, images, the public API and MCP. Parity tests compare it against today's site and API. - Sync and ingestion on Workers: cron plus Queues. Then disable the Vercel cron.
- Admin behind Cloudflare Access, with uploads going straight to the R2 binding. The upload Worker and its token go away.
- Cut over with the rebrand.
allthings.devserves the Worker,allthingsweb.devredirects, and the Vercel project andapp/are deleted.
The rebrand (flag, /brand, new pages) gets built on the new stack instead of in Next, so nothing is built twice.
Enforced budgets (CI on every PR preview)
- Cached public page: TTFB at most 50 ms and full load at most 70 ms from the GitHub runner, with the median of several requests checked against the budget.
- At most 10 KB of JavaScript on public pages; HTML and CSS size budgets per page.
- No external fonts or scripts without an explicit allowlist.
Open questions for Erik and Andre
- Templating:
@kitajs/html(typed JSX to strings, my recommendation) or Hono JSX? - Admin auth: Cloudflare Access instead of Stack Auth? And do member profiles still need any sign-in?
- Data layer: move to Effect SQL plus Schema (recommended), or keep Drizzle for queries during the move?
- Rebrand timing: build it only on the new stack and launch both together (recommended)?
- CI deploy credentials: CI needs a Cloudflare API token. Cloudflare only mints a permissioned token with the account's Global API Key, which means one dashboard visit, once.
Once these are settled, the first PR is core/ with Schema models and contract tests, a small and reviewable step.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
esthor commentedon Oct 4, 2026
The open questions are settled:
@kitajs/html, with@kitajs/ts-html-pluginso unescaped content fails type checking. The router is Effect's ownHttpRouter/HttpApi; Hono would add nothing./adminmoves behind Cloudflare Access./profileis removed: only 3 accounts ever linked, the last in Sept 2025.@effect/sql-pgover Hyperdrive) withSchemamodels. No Drizzle in the new stack.next.allthings.devonce the read-only site reaches parity.infra/stacks/github.ts(Deploy PR previews and staging to the allthings Cloudflare account #72), an account-owned token scoped to what the stack declares.Phase 1 has started with #71:
core/, the public contract in Effect Schema, with contract tests against the live API.Add core: the public contract in Effect Schema (#71)