A production-minded starter for full-stack Cloudflare apps: React Router on Workers, Durable Objects, D1, Drizzle, Hono, typed bindings, Turborepo, and Alchemy deploys. Copy it, rename it, ship it. The demo covers SSR, D1, service bindings, Durable Objects, and WebSockets.
- React Router 8 on Workers — streaming SSR, Tailwind, typed loaders/actions, form actions.
- Durable Object example — Socka WebSockets + DO SQLite on
/chat(chatroom-do), with auth session gating via service bindings. - Better Auth (
auth-worker) — email/password + optional Google/GitHub, account display names, and admin APIs used by the web admin UI (/admin); anonymous guests on/chat(7-day sliding session, random names likeCoastal-Falcon). - D1 + Drizzle — root app DB (
/visitorscounter + chat room registry) plus separate auth D1 (@internal/auth-db). - Typed bindings — package-local
alchemy.run.ts→ Workerenvtypes. - Deploy story — Turbo + Alchemy, staging/production, PR previews (details below only when you need them).
| Goal | Where |
|---|---|
| Web routes, SSR, bindings, forms | apps/web/README.md |
| Auth, OAuth, admin, anonymous chat guests | docs/oauth-setup.md (Google/GitHub) · .agents/skills/auth-setup/SKILL.md · .env.example |
| GitHub Environments, rulesets, what runs in CI, custom domains | docs/github-admin.md |
.env.local / staging / prod secrets |
.env.example · .agents/skills/workers-env-local/SKILL.md |
| Full rebrand (package names, UI copy) | .agents/skills/project-init/SKILL.md |
| Typegen cadence, Turbo deploy order, generated artifacts | .agents/skills/multiworker-workflow/SKILL.md |
| Cursor / IDE rules look wrong after clone | bun run agents:link · .agents/README.md |
Bun: use the version in root package.json → packageManager (CI matches it).
Prerequisites: Bun (see packageManager above), git, a Cloudflare account for local Alchemy resources, and Portless for the default local HTTPS dev URL (https://<PRODUCT_PREFIX>-web.localhost, e.g. starter-web.localhost with stock PRODUCT_PREFIX) — install / service / trust in CONTRIBUTING.md — Local HTTPS dev. For plain http://localhost only, set LOCAL_PORTLESS=off in .env.local (or use bun run setup:local → category Local dev HTTPS (Portless)).
Create a repo from the template:
gh repo create my-project --template your-org/cloudflare-multiworker-template --public
cd my-projectOr Use this template on the GitHub repository (replace your-org with the template owner).
bun run quickstartThat installs dependencies if node_modules is missing, fills missing regeneratable keys in .env.local, runs a dev preflight, then starts the dev stack. It does not rotate existing secrets.
On a new machine you may need Alchemy linked to Cloudflare once:
bun alchemy configure
bun alchemy loginThen rerun bun run quickstart (or bun run dev if .env.local is already set).
Open the URL Vite prints — with Portless (default), Local: is https://<PRODUCT_PREFIX>-web.localhost/ (see CONTRIBUTING — Portless); otherwise often http://localhost:5173. Try /, /visitors, /chat (anonymous guest sign-in), /login, /account. Set AUTH_BOOTSTRAP_ADMIN_EMAILS in .env.local to access /admin after signing in with that email.
Before meaningful deploys, name your product so Cloudflare/Alchemy names match your app.
You need a Cloudflare API token and Account ID from the dashboard (this template does not create tokens — step-by-step). Put them in .env.staging / .env.production, or once in the machine-wide account file (bun run setup:account, paths in .env.example) so you do not duplicate CLOUDFLARE_* / ALCHEMY_STATE_TOKEN in every repo. Token and account must be the same Cloudflare account.
Suggested order for staging + prod:
bun run setup:account(optional) — sharedCLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_API_TOKEN,ALCHEMY_STATE_TOKENon this machine.bun run setup:stagingthenbun run github:sync:staging(orbun run onboard:staging).bun run setup:prodthenbun run github:sync:prod(orbun run onboard:prod).
Per-environment secrets (ALCHEMY_PASSWORD, CHATROOM_INTERNAL_SECRET, BETTER_AUTH_SECRET, AUTH_ADMIN_SECRET, optional AUTH_BOOTSTRAP_ADMIN_EMAILS, optional WEB_*) stay in each stage dotfile (or GitHub Environments after sync). No auth URL env var — Alchemy derives the public auth URL from the local web origin, WEB_DOMAINS, or web workers.dev (see auth-setup).
With gh authenticated and repo admin rights, from a trusted machine:
bun run onboard:staging # sync staging → push/merge to `main` deploys staging after CI
bun run onboard:prod # sync production → deploys from `production` branch (see docs)bun run github:setup prints a fuller Actions checklist. Workflow behavior, DEPLOY_ENABLED, fork vs same-repo PR previews, rulesets, and AUTO_PRODUCTION_PR: docs/github-admin.md.
- Local: Alchemy auth —
bun alchemy configure,bun alchemy login; optionalCLOUDFLARE_*in.env.localorsetup:account(machine-wide file). Missing generated secrets —bun run setup:localor rerunquickstart. onboard:staging: Cloudflare credentials in.env.staging,setup:staging, or the machine account file;gh auth login.onboard:prod: same for.env.production, orONBOARD_PROD_COPY_CF=1to copy token/account from.env.staging(non-interactive).- Wrong account: token and Account ID must match the same Cloudflare account.
Alchemy app ids (e.g. skybook-frontend, skybook-database) come from one place:
- Set
PRODUCT_PREFIXinpackages/alchemy-utils/src/worker-peer-scripts.ts(defaultstarter→ your slug). - Run
bun run typegen. - Adjust visible product copy when you want.
Workspace package names and Turbo --filter values (e.g. @internal/web) are separate from those ids. Full checklist: .agents/skills/project-init/SKILL.md.
Use gitignored stage files from the repo root:
bun run setup:staging
bun run deploy:staging
bun run setup:prod
bun run deploy:prodEach command runs the full Turbo graph (shared Alchemy state, D1 + migrations, workers/DOs, then the web app with bindings). Required keys: .env.example. Keep ALCHEMY_PASSWORD the same everywhere that stage deploys (local, CI, teammates).
Custom domains (WEB_* env vars): docs/github-admin.md.
Optional PostHog: leave keys empty to stay dark; wiring/removal notes: apps/web/README.md.
Only apps/web faces the internet. Every other worker is a service binding (apps/web/alchemy.run.ts). The web worker gates chat sessions with auth-worker, then forwards WebSocket upgrades to chatroom-do with attested headers.
---
config:
layout: dagre
theme: neutral
---
flowchart TB
Browser["Browser"]
PostHog["PostHog ingest"]
subgraph webLayer["apps/web"]
AppDB[("App D1<br>visits · chat room registry")]
Web["React Router"]
end
subgraph authLayer["auth-worker"]
AUTH["Better Auth API"]
AuthDB[("Auth D1<br>users · sessions · OAuth")]
AuthKV[("AUTH_KV<br>trusted origins")]
end
subgraph chatLayer["chatroom-do"]
CHAT["Chat Worker<br>WebSockets · Socka"]
ChatDO["ChatroomDo"]
ChatSQL[("SQLite<br>chat per room")]
end
PH["posthog-proxy"]
Web --> AppDB & AUTH & CHAT & PH
AUTH --> AuthDB & AuthKV
CHAT --> ChatDO --> ChatSQL
CHAT -. service probe .-> AUTH
Browser -- HTTPS --> Web
PH -- forward /d/* --> PostHog
classDef external fill:#f8fafc,stroke:#94a3b8,color:#334155
class Browser,PostHog external
Common request paths (detail in apps/web/README.md):
| Flow | Path |
|---|---|
| Auth / sessions | Browser → web /api/auth/* → AUTH.fetch → auth-worker (Better Auth + auth D1). Loaders use env.AUTH via @internal/auth-client. |
| Chat WebSocket | Browser → web /api/ws/* → session or attest token check in web → CHATROOM.fetch with attestation headers → chatroom-do verifies the worker-to-worker request. |
chatroom-do also has an AUTH binding for the /service-ack provider probe; normal chat WebSocket identity is resolved by the web worker before forwarding.
Bindings and route wiring: apps/web/alchemy.run.ts, apps/web/workers/hono-app.ts. Adding a worker: Adding workers below.
├── apps/
│ └── web/ # React Router app + Worker entry
├── workers/
│ ├── auth-worker/ # Better Auth API, admin routes, trusted origins KV
│ └── posthog-proxy/ # Optional PostHog reverse proxy
├── durable-objects/
│ └── chatroom-do/
├── packages/
│ ├── alchemy-utils/ # PRODUCT_PREFIX, app ids, alchemy-cli
│ ├── auth-client/ # getSession, createAuthClient, binding headers for AUTH.fetch
│ ├── auth-db/ # Better Auth D1 schema + migrations
│ ├── chat-contract/
│ ├── db/ # App D1 schema + Drizzle migrations (/visitors + chat room registry)
│ ├── scripts/ # quickstart, setup, onboard, GitHub sync helpers
│ └── state-hub/ # shared remote Alchemy state (non-local STAGE)
├── stacks/ # admin / GitHub sync (Alchemy)
├── .agents/ # AI rules + skills (human playbooks too)
├── .cursor/ # Cursor env + symlinks to .agents/
└── .claude/ # optional Claude Code symlinks
Entry points: apps/web/alchemy.run.ts, apps/web/workers/app.ts, packages/db/src/schema.ts, packages/alchemy-utils/src/worker-peer-scripts.ts.
From the repo root:
bun run typegen
bun run typecheck
bun run lint
bun run buildRun typegen after routes, alchemy.run.ts, or binding/env changes. Run bun run db:generate after editing packages/db/src/schema.ts; run bun run db:generate:auth after editing packages/auth-db/src/schema.ts. Do not hand-edit Drizzle SQL/snapshots, React Router +types, or .alchemy/.
Bindings in app code:
import { env } from "cloudflare:workers";Do not read Worker bindings from React Router loader/action context in this repo.
bunx turbo gen durable-objectThen: add the package to root dev filters if it should run locally; fix turbo.json deploy/destroy order as needed; add a workspace dep from apps/web if the web app uses it; import its ./alchemy from apps/web/alchemy.run.ts; run bun run typegen and bun run typecheck.
Details: .agents/skills/durable-object-package/SKILL.md, .agents/skills/web-alchemy-bindings/SKILL.md, .agents/skills/worker-rpc-turbo/SKILL.md.
| Area | Commands |
|---|---|
| Dev | dev, quickstart, build, typegen, typecheck, lint, clean — dev uses Portless HTTPS by default (CONTRIBUTING; opt out with LOCAL_PORTLESS=off) |
| Deploy | deploy:staging, deploy:prod, deploy:preview, destroy:*, deploy:preflight:* |
| GitHub Environments | github:setup, github:sync:staging, github:sync:prod, github:sync, github:env:*, github:sync:config |
| DB | db:generate, check:drizzle-generated |
More context: .agents/skills/multiworker-workflow/SKILL.md, docs/github-admin.md.
CONTRIBUTING.md— PRs and checks.AGENTS.md— index for AI assistants;.agents/skills/are deep playbooks (optional for humans).
Cloudflare Workers + Durable Objects + React Router 8 + Hono + D1 + Drizzle + Turborepo + Alchemy + Biome + Bun + Zod.
Real infra + demo routes: treat as a starting point. This repository’s stock workflows use GitHub Environments for same-repo PR previews (staging), production deploys from production, and guardrails so fork PRs never receive preview deploy secrets. Auth is included for demonstration (Better Auth + admin UI + anonymous chat guests)—harden for production (CSP, rate limits, OAuth review, least-privilege tokens). See .agents/skills/auth-setup/SKILL.md, docs/github-admin.md, and .agents/skills/workers-env-local/SKILL.md.
See CONTRIBUTING.md.
MIT
