ShipAgent

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>-server runs 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 into cloudflare/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

  1. Checks. pnpm lint and pnpm test pass, the setup skills passed, the repo name does not end in -starter.
  2. Production env file. Agent: creates cloudflare/.env.production.local from .env.local and changes what differs in production: NEXT_PUBLIC_SITE_URL (the address you will test on), DATABASE_URL (the production database), a fresh BETTER_AUTH_SECRET and CRON_SECRET, NEXT_PUBLIC_OPTIMIZED_IMAGES=false. Test payment keys are fine until launch.
  3. 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>-tags and the Hyperdrive config <worker>-db, fills the placeholders in the wrangler files, generates CF_ENV_KEY into ~/.config/cloudflare/env-keys/<worker>, and sets three GitHub secrets: CF_ENV_KEY, CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID. It is safe to rerun.
  4. Seal. Agent: encrypts the env file into cloudflare/env.production.enc and checks that it decrypts with the right number of variables.
  5. Tables. Agent: runs the migrations against the production database.
  6. Commit, push, deploy. Agent: commits and pushes, then starts the workflow by hand (gh workflow run cloudflare.yml) and watches it.
  7. Verify on workers.dev. Agent: runs the health check. If NEXT_PUBLIC_SITE_URL is the workers.dev address, it also signs in, runs one generation and places one Stripe test order there.
  8. Your domain. You: Dashboard → Workers & Pages → <worker> (the front Worker, never <worker>-server) → Settings → Domains & Routes → Add → Custom domain → your hostname. Add www. as a second entry if you want it. Cloudflare creates the DNS record and the certificate. Agent: sets NEXT_PUBLIC_SITE_URL to 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.
  9. Scheduled jobs. Agent: adds the /api/cron/credits job once your site sells subscriptions or uses PayPal. Each job needs two matching entries: triggers.crons in wrangler.jsonc and the CRONS table in cloudflare/middleware.ts. The starter ships with none.
  10. 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 main moved 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".