Deploy to Cloudflare Workers
What the agent does and what you do to put the site on Cloudflare Workers, how deploys are scheduled, and what to do when a run fails.
Outcome: the health check answers 200 with "status":"ok" and "database":{"status":"ok"…}, first on the workers.dev address, then on your domain:
curl -sS -w '\nHTTP %{http_code}\n' https://<worker>.<subdomain>.workers.dev/api/health
curl -sS -w '\nHTTP %{http_code}\n' https://<your-domain>/api/health
The agent does not report the deploy as done until both answer 200.
Time: about 20 minutes of agent time plus one GitHub Actions run (10 to 25 minutes). You create one API token and click once to attach the domain.
Tell the agent: "Deploy to Cloudflare." It follows skills/deploy-cloudflare/SKILL.md. CLOUDFLARE.md in the repo is the short version.
How it works
- One build produces two Workers.
<worker>is the public one: your address, static files, the page cache and scheduled jobs (wrangler.jsonc,cloudflare/middleware.ts).<worker>-serverruns the Next.js server and has no public address (wrangler.server.jsonc,cloudflare/server.ts). Each deploy pins the two to the same build. - GitHub Actions builds and deploys (
.github/workflows/cloudflare.yml), not Cloudflare's own build service. - Production settings travel in git, encrypted. The agent writes them into
cloudflare/.env.production.local(never committed) and seals them intocloudflare/env.production.enc(committed). Only the key,CF_ENV_KEY, is a GitHub secret. - The database is reached through Hyperdrive, Cloudflare's connection pool for Postgres.
What you do
You: a Cloudflare account on the Workers Paid plan. Dashboard → Workers & Pages → Plans (check the current price there). The server Worker is larger than the free plan allows, and the CPU limit set in the wrangler files needs Paid.
You: one API token. My Profile → API Tokens → Create Token → Create Custom Token. Add these permissions, then Account Resources: Include → your account → Continue → Create. Cloudflare shows the token once: copy it and give it to the agent.
| Scope | Permission | Access | Why |
|---|---|---|---|
| Account | Workers Scripts | Edit | deploys both Workers |
| Account | Workers R2 Storage | Edit | creates the page-cache bucket; uploads static files over 25 MiB |
| Account | D1 | Edit | creates the cache-tag database |
| Account | Hyperdrive | Edit | creates the database connection pool |
| Account | Account Settings | Read | finds your account |
| User | Memberships | Read | lets wrangler find your account on its own |
| User | User Details | Read | the same lookup |
A shorter route that also works: start from the Edit Cloudflare Workers template and add D1 Edit and Hyperdrive Edit. If a permission name differs in the form, pick the closest one (check the dashboard, menus move). The agent verifies the token before it uses it.
You: the GitHub repository. The agent can create it (gh repo create <owner>/<name> --private --source . --push) or use one you have. Its name must not end in -starter: the workflow skips such repos and still reports success.
You: confirm a one-time code in the browser if the agent's gh login lacks the workflow scope (gh auth refresh -h github.com -s workflow).
You: a domain whose DNS is on Cloudflare, and one click to attach it (step 8 below). See Domain and DNS.
The agent asks for each of these only when it reaches the step that needs it, one at a time. It never prints the token, the CF_ENV_KEY or any value from the env file back to you.
What the agent does
- Checks.
pnpm lintandpnpm testpass, the setup skills passed, the repo name does not end in-starter. - Production env file. Agent: creates
cloudflare/.env.production.localfrom.env.localand changes what differs in production:NEXT_PUBLIC_SITE_URL(the address you will test on),DATABASE_URL(the production database), a freshBETTER_AUTH_SECRETandCRON_SECRET,NEXT_PUBLIC_OPTIMIZED_IMAGES=false. Test payment keys are fine until launch. - Cloudflare resources. Agent: runs
node scripts/cf-new-site.mjs <worker> --repo <owner>/<repo>. It creates the R2 bucket<worker>-inc-cache, the D1 database<worker>-tagsand the Hyperdrive config<worker>-db, fills the placeholders in the wrangler files, generatesCF_ENV_KEYinto~/.config/cloudflare/env-keys/<worker>, and sets three GitHub secrets:CF_ENV_KEY,CLOUDFLARE_API_TOKEN,CLOUDFLARE_ACCOUNT_ID. It is safe to rerun. - Seal. Agent: encrypts the env file into
cloudflare/env.production.encand checks that it decrypts with the right number of variables. - Tables. Agent: runs the migrations against the production database.
- Commit, push, deploy. Agent: commits and pushes, then starts the workflow by hand (
gh workflow run cloudflare.yml) and watches it. - Verify on workers.dev. Agent: runs the health check. If
NEXT_PUBLIC_SITE_URLis the workers.dev address, it also signs in, runs one generation and places one Stripe test order there. - Your domain. You: Dashboard → Workers & Pages →
<worker>(the front Worker, never<worker>-server) → Settings → Domains & Routes → Add → Custom domain → your hostname. Addwww.as a second entry if you want it. Cloudflare creates the DNS record and the certificate. Agent: setsNEXT_PUBLIC_SITE_URLto the domain, registers the production Stripe webhook for it, asks you to add the domain's callback URLs at each login provider you use, re-seals, deploys, and runs the health check on the domain. - Scheduled jobs. Agent: adds the
/api/cron/creditsjob once your site sells subscriptions or uses PayPal. Each job needs two matching entries:triggers.cronsinwrangler.jsoncand theCRONStable incloudflare/middleware.ts. The starter ships with none. - What is left. Agent: tells you what remains before launch: live payment keys, a re-seal and a deploy.
When a push goes live
Pushing to main does not deploy. The workflow runs:
- once a day at 09:07 UTC. It first checks whether
mainmoved since the last successful deploy and skips the build if not. A day without changes costs a few seconds of Actions time. - when someone runs it.
gh workflow run cloudflare.yml, or GitHub → Actions → cloudflare → Run workflow. A manual run always deploys.
Use the manual run when a change cannot wait: the first deploy, a new domain, a fix for something that is broken for users. Otherwise let changes collect until the daily run. The reason is the next point.
There is no version skew protection. A visitor who has a page open while a deploy finishes may see their next form submit or button press fail once. Fewer deploys means fewer such failures.
Watch a run on GitHub → Actions, or ask the agent: gh run list --workflow cloudflare.yml -L 3.
Changing a production setting later
Ask the agent. It edits cloudflare/.env.production.local, re-seals it, commits cloudflare/env.production.enc, and deploys. Every NEXT_PUBLIC_* value is baked in at build time, so a change only shows after the next deploy.
Rolling back
Ask the agent to revert the bad commit and run the workflow. Do not roll back one Worker by hand in the Cloudflare dashboard: the front Worker is pinned to the server built with it.
Costs
Cloudflare has no hard spending cap. Set a budget alert in the dashboard's billing settings (check the dashboard, menus move). Each Worker already has a CPU limit of 30 seconds per request (limits.cpu_ms: 30000).
Cloudflare or Vercel
From skills/reference/platform-differences.md. The agent follows these rules when it changes code; you only need them to choose.
| Topic | Vercel | Cloudflare Workers |
|---|---|---|
| Deploy trigger | every push to main (after vercel.json is deleted) |
daily 09:07 UTC when main moved, or gh workflow run cloudflare.yml |
| Build | pnpm build on Vercel |
pnpm cf:build on GitHub Actions, then two Workers deployed together |
| Production settings | project settings; a change needs a redeploy | encrypted file in git; re-seal and redeploy |
| Reading site files (posts, copy) | from disk | no disk: folders listed in cloudflare/site-data.json ship with the build |
Native modules (sharp, canvas) |
work | do not run; process images through an API |
Image optimization (/_next/image) |
works | not available (NEXT_PUBLIC_OPTIMIZED_IMAGES=false) |
| Database connection | one shared client per instance | one connection per request through Hyperdrive |
| Scheduled jobs | crons in vercel.json |
triggers.crons in wrangler.jsonc plus the CRONS table |
prebuild / postinstall hooks |
run | do not run |
| Version skew protection | available | none |
| Runtime limits | function duration per route | 30 s CPU per request; 64 MiB per Worker script; 25 MiB per static file (larger ones go to R2) |
| Visitor country and IP headers | set by Vercel | set by cloudflare/middleware.ts under the same names |
When it fails
The agent reads the run log (gh run view <id> --log-failed) and fixes most of these itself. The ones that need you are marked.
| What you see | Cause | Fix |
|---|---|---|
| The run is green within seconds and the deploy job was skipped | the repo name ends in -starter, or a scheduled run found main unchanged |
rename the repo; a manual run always deploys |
Authentication error [10000] |
wrong token, the env key pasted into the token secret, or a missing permission | You: create the token again with the table above; the agent compares the logged token length |
refusing to allow an OAuth App to create or update workflow |
gh lacks the workflow scope |
You: confirm the one-time code after gh auth refresh -h github.com -s workflow |
cf-env: CF_ENV_KEY is not set or Unsupported state or unable to authenticate data |
the secret is missing, or the file was sealed with another key | the agent re-seals with the key file and resets the secret |
Killed or an out-of-memory error |
the runner ran out of memory | the agent reruns once (gh run rerun <id>) |
Error 10021 or a sharp build error |
an image library that cannot run on Workers reached the bundle | the agent removes it and uses an image API |
| Error 10027, script too large | the server Worker is over 64 MiB | the agent moves large data out of the bundle |
Health 503, database: down |
no Hyperdrive binding yet, or the database refuses the connection | the agent reruns cf-new-site.mjs and deploys; check the production DATABASE_URL |
Health 200 but database: skipped |
DATABASE_URL is missing from the sealed file |
the agent adds it, re-seals, deploys |
| Sign-in fails on the address you test | NEXT_PUBLIC_SITE_URL is a different address |
the agent sets it to the address in use and deploys |
| The custom domain is refused in the dashboard | a DNS record for that hostname already exists | You: delete that record under the domain's DNS → Records, then add the custom domain again |
| No scheduled run today | GitHub occasionally drops scheduled runs | run the workflow by hand if main moved |
| An open page fails its next submit right after a deploy | no version skew protection | expected; deploy less often |
The skill lists every known failure with its fix: skills/deploy-cloudflare/SKILL.md, "When a check fails".