Skip to content

Plan: move the site to Cloudflare Workers with Effect v4 and Alchemy #70

Open

Description

@esthor

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 on media.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

  1. Core. core/ with Schema models and data access for events, talks, speakers, hosts and images, with contract tests against today's API responses.
  2. 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.
  3. Sync and ingestion on Workers: cron plus Queues. Then disable the Vercel cron.
  4. Admin behind Cloudflare Access, with uploads going straight to the R2 binding. The upload Worker and its token go away.
  5. Cut over with the rebrand. allthings.dev serves the Worker, allthingsweb.dev redirects, and the Vercel project and app/ 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

  1. Templating: @kitajs/html (typed JSX to strings, my recommendation) or Hono JSX?
  2. Admin auth: Cloudflare Access instead of Stack Auth? And do member profiles still need any sign-in?
  3. Data layer: move to Effect SQL plus Schema (recommended), or keep Drizzle for queries during the move?
  4. Rebrand timing: build it only on the new stack and launch both together (recommended)?
  5. 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.

Activity

  1. esthor commented on Oct 4, 2026

    @esthor
    ContributorAuthor

    The open questions are settled:

    1. Templating: @kitajs/html, with @kitajs/ts-html-plugin so unescaped content fails type checking. The router is Effect's own HttpRouter/HttpApi; Hono would add nothing.
    2. Admin sign-in: Stack Auth goes away entirely.
      • /admin moves behind Cloudflare Access.
      • Self-serve /profile is removed: only 3 accounts ever linked, the last in Sept 2025.
      • Speaker data comes from Luma sync and X enrichment. Organizers curate it through the authenticated admin MCP/CLI.
    3. Data layer: Effect SQL (@effect/sql-pg over Hyperdrive) with Schema models. No Drizzle in the new stack.
    4. Rebrand: built only on the new stack and launched with the cutover. Progress is visible before then through per-PR preview stages, and on next.allthings.dev once the read-only site reaches parity.
    5. CI deploy credentials: declared in 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions