| name | prepare-cloudflare-production-deployment |
| description | Source-of-truth runbook for preparing this Vinext Cloudflare Workers SaaS template for production deployment. Use when setting up or auditing Cloudflare MCP resources, wrangler.jsonc bindings, Worker secrets, Turnstile, Email Sending, GitHub Actions secrets/variables, or GitHub CLI deployment wiring for this repository. |
Prepare Cloudflare Production
Overview
Use this skill as the source of truth for production deployment. Prefer Cloudflare MCP for Cloudflare account resources and gh for GitHub repository secrets/variables; edit repo files directly for project branding, wrangler.jsonc, and workflow changes.
Never print secret values. Ask for missing secret values instead of inventing placeholders, and confirm before creating paid, destructive, or externally visible resources.
Preflight
- Read
wrangler.jsonc, package.json, .github/workflows/deploy.yml, src/constants.ts, src/app/layout.tsx, src/components/footer.tsx, AGENTS.md, and cms.config.ts when relevant.
- Check and confirm authenticated accounts before creating, updating, or deleting anything:
gh auth status
gh repo view --json nameWithOwner,url
Use Cloudflare MCP to identify the authenticated Cloudflare account. Tell the user the Cloudflare account id/name/email available from MCP and the GitHub account/repository from gh, then ask whether both are correct. Stop if the user says either account is wrong.
-
Confirm or remind the user about the required production customization checklist:
src/constants.ts has project details.
- Read
SITE_URL from src/constants.ts, derive the hostname, and check the domains/zones available in the authenticated Cloudflare account. Tell the user which matching or closest Cloudflare zone/domain was found and ask them to confirm it before proceeding. If the SITE_URL domain is not available in Cloudflare, stop and ask the user which Cloudflare zone/domain to use or whether they need to add the domain to Cloudflare first.
- When the app will use Cloudflare Images, verify Images against that same production zone/hostname, not only against the account. Confirm the
SITE_URL hostname belongs to a Cloudflare zone in the authenticated account and is proxied or attached as the Worker custom domain/route that production will use. Then verify the account-level Images API works for that account with /accounts/{account_id}/images/v1/variants or /accounts/{account_id}/images/v1/stats. If custom-domain image delivery is expected, explicitly confirm the production zone can serve Images URLs at https://<SITE_URL_HOSTNAME>/cdn-cgi/imagedelivery/<ACCOUNT_HASH>/<IMAGE_ID>/<VARIANT_NAME>; Cloudflare supports this only for customer domains under the same account as the Images account. If the domain is in a different account, not proxied through Cloudflare, or not the domain being deployed to, stop and ask which zone/domain should be used before proceeding.
- Read
package.json, tell the user the current name value, and ask them to confirm it is the intended production project name. This value controls generated deploy-size metrics and package metadata, so do not proceed if it still identifies the reused template. If the name is still cloudflare-workers-nextjs-saas-template, stop and ask the user for the real project name and production domain before editing Cloudflare resources, queue names, bindings, or deployment metadata.
- Check queue names in
wrangler.jsonc, especially queues.producers[].queue and queues.consumers[].queue. Queue names must be renamed to match the new production project name; do not leave template queue names such as cloudflare-workers-nextjs-saas-template-scheduler in a production project unless the user explicitly confirms that is the real project name.
AGENTS.md has the project specification for AI coding agents.
src/components/footer.tsx has project links and details.
src/app/globals.css color palette has been reviewed.
src/app/layout.tsx metadata has project details.
cms.config.ts has been reviewed and updated if needed.
-
Check local validation tools when deployment-related files change:
pnpm run check:vinext
pnpm run typecheck
pnpm run build
- Use Cloudflare MCP
search before execute to verify current endpoint shapes for any Cloudflare operation.
- Collect required inputs:
- Project name and production domain.
- Cloudflare account id, or confirm the MCP account id.
- Zone id if CDN purge or Email Sending domain setup is needed.
- Sending email address, reply-to address, and sender display name.
- GitHub repository owner/name if it differs from the current checkout.
- Secret values:
CLOUDFLARE_API_TOKEN, TURNSTILE_SECRET_KEY, and any application secrets such as Stripe or OAuth credentials.
- Remind the user that
CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are needed in two places: GitHub Actions for deploy/migrations/cache purge, and the deployed Worker runtime for admin Cloudflare Queue message preview.
Automation Map
| Deployment task | Primary tool | Agent handling |
|---|
| Confirm production customization checklist | File reads and user confirmation | Required before deployment. Remind the user about any unchecked items and update files when they provide project details. |
Customize src/constants.ts, package.json, AGENTS.md, footer, palette, metadata, cms.config.ts | File edits | Automatable after project details are known. |
| Create D1 database | Cloudflare MCP | Automatable with POST /accounts/{account_id}/d1/database; update wrangler.jsonc with database_name and database_id. |
| Create KV namespace | Cloudflare MCP | Automatable with POST /accounts/{account_id}/storage/kv/namespaces; update wrangler.jsonc namespace id. |
| Create R2 bucket | Cloudflare MCP | Automatable with POST /accounts/{account_id}/r2/buckets; update wrangler.jsonc bucket name. |
| Create Queue resources | Cloudflare MCP or Wrangler | Automatable after the final project name is known. Queue names in wrangler.jsonc must match the production project name, for example <project-name>-scheduler; if the project name is still cloudflare-workers-nextjs-saas-template, ask for the real project name and production domain first. |
| Enable or verify Cloudflare Images | Cloudflare MCP or dashboard | Verify both the account-level Images API and the production SITE_URL zone/domain. MCP can list/use Images endpoints under /accounts/{account_id}/images/v1; custom-domain delivery requires a proxied/customer domain in the same Cloudflare account as Images, and billing acceptance may still require dashboard interaction. |
| Onboard Email Sending domain | Cloudflare MCP or dashboard | MCP supports Email Sending subdomain create/preview/fix/status endpoints under zones. Domain ownership, DNS propagation, plan gating, or account approval can require waiting or dashboard follow-up. |
Update email vars and send_email.allowed_sender_addresses | File edits | Automatable after sender values are known. |
| Create Turnstile widget | Cloudflare MCP | Automatable with POST /accounts/{account_id}/challenges/widgets; set site key as GitHub variable and secret key as Worker secret. |
Set TURNSTILE_SECRET_KEY Worker secret | Cloudflare MCP or Wrangler | Automatable through Worker Script secrets API after the Worker script exists, or with wrangler secret put. |
Update wrangler.jsonc account id, bindings, vars, and project name | File edits | Automatable. Run pnpm run cf-typegen if bindings change. |
| Create Cloudflare API token | Cloudflare dashboard and gh | Always ask the user to create/provide the token manually. Store the provided token in GitHub Actions with gh; do not try to create API tokens through Cloudflare MCP. |
Add CLOUDFLARE_API_TOKEN GitHub secret | gh | Automatable with gh secret set CLOUDFLARE_API_TOKEN --repo OWNER/REPO. This powers deployment, migrations, and cache purge in GitHub Actions. |
Add CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_ZONE_ID, NEXT_PUBLIC_TURNSTILE_SITE_KEY GitHub variables | gh | Automatable with gh variable set NAME --body VALUE --repo OWNER/REPO. CLOUDFLARE_ACCOUNT_ID powers GitHub Actions and should match the account used by wrangler.jsonc. |
| Set Worker runtime Cloudflare API credentials | Cloudflare MCP or Wrangler | Required for the admin scheduled jobs page to preview Cloudflare Queue payloads. Set CLOUDFLARE_ACCOUNT_ID as a Worker variable and CLOUDFLARE_API_TOKEN as a Worker secret for the deployed Worker. |
Push to main and deploy | Git and GitHub Actions | Automatable only after user approval for push/deploy. Use gh run list, gh run watch, and gh run view --log-failed to monitor. |
Cloudflare MCP Patterns
Use idempotent create-or-reuse behavior:
- List existing resources by name.
- Reuse exact matches.
- Create only when missing and the user has approved the final names.
- Store returned ids in
wrangler.jsonc.
Common endpoint families to search and use:
Zones/domains: /zones
D1: /accounts/{account_id}/d1/database
KV: /accounts/{account_id}/storage/kv/namespaces
R2: /accounts/{account_id}/r2/buckets
Queues: /accounts/{account_id}/queues
Turnstile: /accounts/{account_id}/challenges/widgets
Worker secrets: /accounts/{account_id}/workers/scripts/{script_name}/secrets
API tokens: /accounts/{account_id}/tokens
Email Sending: /zones/{zone_id}/email/sending/subdomains
Images: /accounts/{account_id}/images/v1
Images variants and stats: /accounts/{account_id}/images/v1/variants, /accounts/{account_id}/images/v1/stats
Images custom-domain delivery check: resolve SITE_URL hostname to a same-account zone, then verify or document delivery through https://<hostname>/cdn-cgi/imagedelivery/<ACCOUNT_HASH>/<IMAGE_ID>/<VARIANT_NAME>
Cache purge: /zones/{zone_id}/purge_cache
When a Cloudflare step is blocked by billing, product enablement, token permissions, DNS propagation, or account approval, report the exact blocker and give the smallest dashboard action needed.
Secret Handling
Do not print secret values in chat, logs, diffs, or command output. Prefer piping secret values directly into the destination tool.
CLOUDFLARE_API_TOKEN:
- Apologize to the user that, unfortunately, Cloudflare API token creation is not supported by the current Cloudflare MCP workflow. Always ask the user to create/provide this token manually. Do not try to create, roll, or retrieve Cloudflare API tokens through Cloudflare MCP.
- Send the user to
https://dash.cloudflare.com/profile/api-tokens.
- Tell them to click Use template next to Edit Cloudflare Workers.
- Tell them to add these permissions in addition to the template defaults:
Account:AI Gateway:Edit
Account:Workers AI:Edit
Account:Workers AI:Read
Account:Queues:Edit
Account:Vectorize:Edit
Account:D1:Edit
Account:Cloudflare Images:Edit
Account:Workers KV Storage:Edit
Account:Email Sending:Edit
Zone:Cache Purge:Purge
- Tell them to scope the token to the intended Cloudflare account and, when zone permissions are needed, the intended production zone.
- Tell the user this token is used in two contexts:
- GitHub Actions uses
CLOUDFLARE_API_TOKEN for deploy, D1 migrations, and cache purge.
- The deployed Worker uses
CLOUDFLARE_API_TOKEN for the admin scheduled jobs page to preview Cloudflare Queue payloads. Native Queue binding metrics do not need this token, but payload preview does.
- After they provide the token, add it to GitHub Actions without printing it:
gh secret set CLOUDFLARE_API_TOKEN --repo OWNER/REPO
Paste the token only into the secure gh prompt. Verify the secret exists with:
gh secret list --repo OWNER/REPO
- Also set the token as a Worker secret for runtime admin Queue preview:
pnpm wrangler secret put CLOUDFLARE_API_TOKEN
CLOUDFLARE_ACCOUNT_ID:
- Set
CLOUDFLARE_ACCOUNT_ID as a GitHub Actions variable for deploy/migrations:
gh variable set CLOUDFLARE_ACCOUNT_ID --body "$ACCOUNT_ID" --repo OWNER/REPO
- Also make
CLOUDFLARE_ACCOUNT_ID available to the deployed Worker runtime for admin Queue preview. Prefer adding it to wrangler.jsonc under vars when the account id is stable for the project, or set it as a Worker variable through the Cloudflare dashboard/API. Do not treat the account id as a secret, but do verify it matches the account used by wrangler.jsonc.
TURNSTILE_SECRET_KEY:
- Cloudflare MCP can retrieve the secret from a Turnstile widget details response, create a new widget and capture its secret, or rotate a widget secret after user confirmation.
- Prefer reusing the intended existing widget when the domain/name matches. Create or rotate only after confirming with the user, because rotating can invalidate production captcha verification.
- Set the Turnstile site key as
NEXT_PUBLIC_TURNSTILE_SITE_KEY in GitHub repository variables and set the secret key as TURNSTILE_SECRET_KEY in Worker secrets.
GitHub CLI Patterns
Prefer gh for repository configuration:
gh secret set CLOUDFLARE_API_TOKEN --repo OWNER/REPO
gh variable set CLOUDFLARE_ACCOUNT_ID --body "$ACCOUNT_ID" --repo OWNER/REPO
gh variable set CLOUDFLARE_ZONE_ID --body "$ZONE_ID" --repo OWNER/REPO
gh variable set NEXT_PUBLIC_TURNSTILE_SITE_KEY --body "$SITE_KEY" --repo OWNER/REPO
gh variable set NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY --body "$STRIPE_KEY" --repo OWNER/REPO
gh secret list --repo OWNER/REPO
gh variable list --repo OWNER/REPO
Use gh secret set from stdin or an environment variable when handling sensitive values. Do not place secrets in shell history or command output.
Set any required NEXT_PUBLIC_* values as GitHub Actions variables with gh variable set. The deploy workflow auto-forwards matching variables, so do not add individual NEXT_PUBLIC_* entries to .github/workflows/deploy.yml.
Remember that GitHub Actions secrets/variables are not automatically Worker runtime variables. If the admin scheduled jobs page should preview Cloudflare Queue payloads in production, ensure the deployed Worker also has CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN configured at runtime.
Repository Edits
Apply the repo rules from AGENTS.md:
- Keep Vinext commands; do not reintroduce legacy Next.js or OpenNext deploy paths.
- Update
wrangler.jsonc, not worker-configuration.d.ts; run pnpm run cf-typegen after binding changes.
- Preserve existing comments unless they are stale.
- For form or app-specific secrets not mentioned in the README, inspect
.github/workflows/deploy.yml, src/flags.ts, and integrations before deciding which GitHub variables/secrets are needed.
- Run
pnpm run check:vinext, pnpm run typecheck, and pnpm run build when deployment-related files change and time permits.
Deployment Verification
After configuration:
- Run local checks:
pnpm run lint
pnpm run typecheck
pnpm run check:vinext
pnpm run build
- If deploying through GitHub Actions, monitor the workflow:
gh run list --workflow deploy.yml --limit 5
gh run watch RUN_ID --exit-status
gh run view RUN_ID --log-failed
- Confirm remote D1 migrations ran, the Worker deployed, optional cache purge succeeded, and deploy-size metrics were committed or intentionally skipped.