Cloudflare Workers Deploy Skeleton
Get a new Cloudflare Workers project to the state where main push → production URL returns /health 200 and serves SPA HTML, with business logic = zero.
Why do this first: Deployment boundaries (wrangler.jsonc, GH Actions, D1, Cron registration, SPA routing, secure-headers-ready asset serving) are the most expensive things to debug if they break after a codebase is full. Wire them end-to-end on an empty app, then grow logic on top of proven infrastructure.
When to use this skill
- Starting a new Cloudflare Workers project where you intend to serve SPA + API + Cron from one Worker
- Re-scaffolding an existing project that grew organically and now has SPA routing / deploy drift issues
- Onboarding a teammate and you need a known-good baseline to reproduce
Do not use for multi-Worker microservices, Pages-only sites, or projects that don't need D1.
Deliverables (completion criteria)
Run through the steps in references/setup-order.md. You're done when:
curl https://<project>.<cf-subdomain>.workers.dev/healthreturns{"status":"ok"}with HTTP 200curl https://<project>.<cf-subdomain>.workers.dev/returns 200 with SPA HTML containing<h1>...</h1>mainpush → GH Actionsdeploy.ymlruns D1 migrations → deploys Worker, all greenwrangler d1 migrations list <db> --remoteshows0000_initappliedwrangler.jsonchas bothassets.run_worker_first: trueandtriggers.crons.dev.varsis gitignored,.dev.vars.exampleis committeddatabase_idinwrangler.jsoncis a real UUID (not a<placeholder>)
The 3-layer SPA routing dance
This is the thing that breaks silently. Serving a React SPA + a Hono Worker + preparing for secureHeaders to cover the SPA HTML requires three pieces to agree. Miss any one → / returns 404.
| Layer | Location | Setting | Purpose |
|---|---|---|---|
| L1 | wrangler.jsonc |
assets.not_found_handling: "single-page-application" |
Fallback assets to index.html |
| L2 | wrangler.jsonc |
assets.run_worker_first: true |
Worker sees every request before Assets (lets secureHeaders wrap SPA HTML later) |
| L3 | worker/index.ts |
app.notFound(...) → delegate to c.env.ASSETS.fetch (full code in worker-template.md) |
Worker explicitly delegates unmatched routes to the Assets binding |
If any layer is missing and / returns 404, consult references/spa-routing-diagnosis.md.
Core files (copy from references)
These references contain fully-formed, copy-ready templates. Use them verbatim and change only what's marked <...>:
references/wrangler-template.md—wrangler.jsoncwith 3-layer SPA + D1 + Cronreferences/worker-template.md—worker/index.ts+worker/cron.ts+worker/types.ts(single-sourceBindings)references/gh-actions-template.md—.github/workflows/deploy.ymlwith migrations → deploy ordering + concurrencyreferences/tsconfig-and-vite.md— TypeScript strict config + Vite +@cloudflare/vite-pluginreferences/d1-scaffold.md— emptydrizzle/0000_init.sqlto validate the migration pipelinereferences/dev-vars.md—.dev.vars.examplepattern (keys only, no values)
Setup flow
High-level — step-by-step with exact commands in references/setup-order.md:
- User (interactive):
wrangler loginin a browser - User:
wrangler d1 create <db-name>→ copydatabase_idUUID - User: Create CF API token with
Workers Scripts:Edit + D1:Edit + Account Settings:Read + User Details:Read→ GitHub repo secretCLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_ID - Agent: Generate files from references above. Substitute the real
database_idUUID immediately; never leave a<placeholder>inwrangler.jsonc - Agent:
pnpm install→pnpm check→pnpm db:migrate(local) → verify with a dev run - Agent: Commit → push → draft PR → user merges → GH Actions runs → observe deploy success
- User: Look up the assigned production URL in CF Dashboard → Triggers → Route
- Agent: Update
wrangler.jsoncRP_ID/ORIGINto the real production hostname → push → re-deploy
The pitfalls that eat hours
Brief summary; full write-ups in references/pitfalls.md:
pnpm deploycollides with a pnpm built-in subcommand. In the workspace root'spackage.json, use"deploy": "pnpm --filter <pkg> run deploy"with explicitrun- "Edit Cloudflare Workers" API token template lacks
D1:Edit→wrangler d1 migrations apply --remotefails with error 7403 in CI. Add the D1 permission manually when creating the token database_idplaceholder left as<...>inwrangler.jsonc→ deploy fails. Substitute immediately afterwrangler d1 create, don't deferRP_IDlocking rule: If you'll ever use WebAuthn / passkeys, the RP_ID (hostname) must be locked on first deploy. Changing it later invalidates every registered credential. Pin to the productionworkers.devsubdomain or your custom domain from day 1, treat as permanent- Local Cron testing: current toolchains expose
/cdn-cgi/handler/scheduled?cron=<expr>on bothwrangler devand vite-plugin1.x(plus theshotkey inwrangler dev). The legacy0.1.xbaseline has no local Cron endpoint at all (requests fall back to SPA HTML) — see pitfall #5 and thecloudflare-cron-to-discordskill
Scope boundary — what this skill does NOT cover
Authentication (passkeys / sessions / JWT) — deferred to a later phase
Security hardening (
secureHeaders, CSP,app.onError,sessionMiddleware) — build on top of this skeleton in a later phaseDomain schema (tasks, users, etc.) — defer to when you know what the domain actually looks like
drizzle-orm/drizzle-kit— don't install until you have a real schema to generate migrations for. The empty0000_init.sqlvalidates the pipeline without forcing a Chekhov's-gun dependency. When you do adopt drizzle for real schema work, read thecloudflare-d1-drizzle-migrationskill first — D1 has a silent incompatibility with drizzle-kit's generated PRAGMAs that can cascade-delete child data on table-rebuild migrationsToken-less deploys — Workers Builds (Cloudflare's git-connected CI/CD, zero Cloudflare credentials in GitHub) is the alternative to the GH-Actions-with-API-token flow used here; see
cloudflare-workers-builds-keyless-deploy
Build logic on top after deploy is provably working, not before.
References
All references below are concrete, copy-ready templates and diagnostic playbooks:
- setup-order.md — end-to-end setup sequence with exact commands
- wrangler-template.md —
wrangler.jsonctemplate - worker-template.md —
worker/index.ts/cron.ts/types.tstemplates - gh-actions-template.md —
deploy.ymltemplate - tsconfig-and-vite.md — TypeScript + Vite setup
- d1-scaffold.md — empty migration for pipeline validation
- dev-vars.md —
.dev.vars.examplepattern - spa-routing-diagnosis.md — 3-layer SPA 404 troubleshooting
- pitfalls.md — known setup traps with full write-ups