Prepare Cloudflare Production
Overview
Use this skill as the source of truth for production deployment. Cloudflare MCP must be used for Cloudflare account/resource discovery, verification, and supported mutations before using Wrangler, raw API calls, dashboard instructions, or assumptions. Use 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/utils/root-metadata.ts,src/components/footer.tsx,AGENTS.md, andcms.config.tswhen relevant. - Check GitHub auth, then complete the
MCP availability gatebefore any Cloudflare mutation:
bashgh auth status gh repo view --json nameWithOwner,url
Report the Cloudflare account id/name/email 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.tshas project details.- Read
SITE_URLfromsrc/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 theSITE_URLdomain 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_URLhostname 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/variantsor/accounts/{account_id}/images/v1/stats. If custom-domain image delivery is expected, explicitly confirm the production zone can serve Images URLs athttps://<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 currentnamevalue, 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 stillcloudflare-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, especiallyqueues.producers[].queueandqueues.consumers[].queue. Queue names must be renamed to match the new production project name; do not leave template queue names such ascloudflare-workers-nextjs-saas-template-schedulerin a production project unless the user explicitly confirms that is the real project name. - Email Sending must follow the
Email Sendingprocedure below: use the productionSITE_URLzone, expectnotifications.<SITE_URL_HOSTNAME>, and get user confirmation before Cloudflare orwrangler.jsoncchanges. AGENTS.mdhas the project specification for AI coding agents.src/components/footer.tsxhas project links and details.src/app/globals.csscolor palette has been reviewed.src/utils/root-metadata.tsmetadata has project details; both root layouts read it.- The
metatitles and descriptions in every locale catalog undersrc/i18n/messages/*.json(for exampleClient.Landing.meta, plus the auth, legal, and blogmetablocks) still default to template copy. Explicitly ask the user for the production titles and descriptions, and help them update these values in all locale catalogs, not onlyen.json. cms.config.tshas been reviewed and updated if needed.
-
Collect required inputs:
- Project name and production domain.
- Confirmed Cloudflare account id and production zone id.
- Sending email address, reply-to address, and sender display name.
- GitHub repository owner/name if it differs from the current checkout.
- Required values from
Required configuration buckets.
-
Follow
Deployment Verificationafter deployment-related file or runtime configuration changes.
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. |
Update meta titles/descriptions in src/i18n/messages/*.json | File edits | Ask the user for production titles/descriptions, then update every locale catalog. |
| Create D1, KV, R2, or Queue resources | Cloudflare MCP | Use MCP patterns below: list by final production name, reuse exact matches, create only when approved, and write returned ids/names to wrangler.jsonc. Queue names must use the final project name. |
| Enable or verify Cloudflare Images | Cloudflare MCP | Verify both the account-level Images API and the production SITE_URL zone/domain. Billing acceptance may still require dashboard interaction. |
| Verify/onboard Email Sending and update sender config | Cloudflare MCP and file edits | Follow Email Sending. Verify the production-zone subdomain before file edits, and edit sender values only after user confirmation. |
| Create/update Turnstile widget | Cloudflare MCP, then dashboard if blocked | Follow the TURNSTILE_SECRET_KEY section: verify domains before reuse, preserve existing widget settings, and put site key/secret in the correct configuration buckets. |
Update wrangler.jsonc account id, bindings, vars, and project name | File edits | Automatable. Run pnpm run cf-typegen if bindings change. |
| Configure GitHub and Worker secrets/variables | gh, Cloudflare MCP, or wrangler.jsonc | Follow Required configuration buckets. GitHub Actions config and Worker runtime config are separate. |
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
Cloudflare MCP is not optional for Cloudflare operations. Always try it first for account/resource reads and supported writes. Wrangler, raw curl, and dashboard instructions are fallback paths only.
MCP availability gate
At the start of deployment prep, before any Cloudflare mutation:
- Confirm Cloudflare MCP tools are installed/available in the current session.
- Confirm Cloudflare MCP can identify the authenticated Cloudflare account.
- Report the Cloudflare account id/name/email from MCP to the user.
- If MCP is missing, not logged in, or cannot identify the account, warn the user and pause Cloudflare resource changes. The warning must include:
- Which MCP capability is missing or failing.
- That Wrangler/raw API/dashboard fallback can diverge from the required MCP-audited workflow.
- The smallest next action: install/enable Cloudflare MCP, log in to Cloudflare MCP, or explicitly approve a fallback.
Only continue with Wrangler/raw API/dashboard fallback after the user explicitly approves that fallback or MCP cannot support the needed product operation.
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:
textZones/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. Secret creation/provisioning is a user-only manual step unless the secret is returned by an explicitly approved product API flow, such as creating a new Turnstile widget. Prefer secure prompts and never invent placeholders.
Required configuration buckets
Before pushing or deploying through GitHub Actions, explicitly tell the user which values belong in each bucket and whether they already exist:
- GitHub Actions secrets are available only to the GitHub workflow. For the current deploy workflow, the required secret is
CLOUDFLARE_API_TOKEN; the user must create/provide it manually and paste it into the secureghprompt. - GitHub Actions variables are non-secret values available to the GitHub workflow. Set
CLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_ZONE_ID, enabledNEXT_PUBLIC_*values, and remember the deploy workflow auto-forwards repositoryNEXT_PUBLIC_*variables into the build. - Worker runtime secrets are available to the deployed Worker, not to GitHub Actions. Ask the user to supply enabled-feature secrets such as
CLOUDFLARE_API_TOKEN,TURNSTILE_SECRET_KEY,STRIPE_SECRET_KEY, andGOOGLE_CLIENT_SECRETthrough secure prompts or supported MCP secret writes. - Worker runtime variables are non-secret values available to the deployed Worker. Put
CLOUDFLARE_ACCOUNT_IDinwrangler.jsoncundervars(see theCLOUDFLARE_ACCOUNT_IDsection). Prefervarsfor other stable values such asGOOGLE_CLIENT_ID; otherwise use Cloudflare MCP/dashboard/API.
After setting repository configuration, verify without exposing values:
bashgh secret list --repo OWNER/REPO gh variable list --repo OWNER/REPO
After setting Worker secrets, verify behavior by exercising the relevant feature or checking the Worker dashboard secret names. Do not attempt to read secret values back.
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, retrieve, guess, or reuse Cloudflare API tokens through Cloudflare MCP/API or repository history.
- 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:EditAccount:Workers AI:EditAccount:Workers AI:ReadAccount:Queues:EditAccount:Vectorize:EditAccount:D1:EditAccount:Cloudflare Images:EditAccount:Workers KV Storage:EditAccount:Email Sending:EditAccount:Turnstile Sites:EditorAccount:Turnstile Sites:Readplus dashboard access if only reading existing widgetsZone: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_TOKENfor deploy, D1 migrations, and cache purge. - The deployed Worker uses
CLOUDFLARE_API_TOKENfor the admin scheduled jobs page to preview Cloudflare Queue payloads, and for the admin "Purge Cloudflare CDN Cache" action. Native Queue binding metrics do not need this token, but payload preview and the purge do. The purge needsZone:Cache Purge:Purge, resolves its zone from the Workers domain of the site host, and is hidden in the panel when the token is absent.
- GitHub Actions uses
- After they provide the token, add it to GitHub Actions without printing it:
bashgh secret set CLOUDFLARE_API_TOKEN --repo OWNER/REPO
Paste the token only into the secure gh prompt. Verify the secret exists with:
bashgh secret list --repo OWNER/REPO
- Also set the token as a Worker secret for runtime admin Queue preview:
bashpnpm wrangler secret put CLOUDFLARE_API_TOKEN
CLOUDFLARE_ACCOUNT_ID:
- Set
CLOUDFLARE_ACCOUNT_IDas a GitHub Actions variable for deploy/migrations:
bashgh variable set CLOUDFLARE_ACCOUNT_ID --body "$ACCOUNT_ID" --repo OWNER/REPO
-
Set
CLOUDFLARE_ACCOUNT_IDinwrangler.jsoncundervars, with the same value as the top-levelaccount_id. The deployed Worker needs it for these features:- The zone lookup in
getWorkerZoneId(src/lib/cloudflare-api.ts). Without the var, the lookup fails with agetWorkerZoneId: zone lookup failedwarning. The CMS publish then purges stored HTML only in one data center, and the admin "Purge Cloudflare CDN Cache" action is hidden. SettingCLOUDFLARE_ZONE_IDskips this lookup. - The admin scheduled jobs Queue payload preview.
Do not set it as a Worker secret. The account id is not a secret, and a secret with the same name as a
varsentry makeswrangler deployfail. If a Worker secretCLOUDFLARE_ACCOUNT_IDalready exists, delete it before the deploy that adds the var: - The zone lookup in
bashpnpm wrangler secret delete CLOUDFLARE_ACCOUNT_ID
- After the deploy, verify the var: open the admin System page and confirm that "Purge Cloudflare CDN Cache" shows a Run button. Then confirm that the Worker logs have no
getWorkerZoneId: zone lookup failedwarning after a CMS publish.
TURNSTILE_SECRET_KEY:
-
First determine whether Turnstile is enabled by inspecting
src/flags.ts, forms usingsrc/components/captcha.tsx, and the presence ofTURNSTILE_SECRET_KEYin the target environment. If disabled, say the Turnstile values are optional until the feature is enabled. -
Prefer reusing an intended existing widget only when its name and allowed domains match the production hostname. Do not copy a site key from another repo or environment unless the user confirms the widget is intended for the new production hostname.
-
If Cloudflare MCP/API execution is available, use docs search before execute, then list/inspect/update widgets through
/accounts/{account_id}/challenges/widgets. Preserve existing widget settings and domains; add the production hostname only after user confirmation because this changes an externally visible security control. -
If Cloudflare MCP/API execution is not available or the token lacks Turnstile permissions, this is a user-only manual step. Do not keep retrying with Wrangler or unrelated APIs. Tell the user that the active Cloudflare MCP/API token needs
Turnstile Sites WriteorAccount Settings Writeto update widgets automatically; without that permission, they must add the hostname manually in the dashboard:- Open
https://dash.cloudflare.com/?to=/:account/turnstile. - Select the existing widget by sitekey, for example
0x4AAAAAAA5QtwiAltpKMppM, or click Add widget if creating a new one. - Go to Settings and Hostname Management.
- Set the widget name to the project or production hostname.
- Add the production hostname from
SITE_URL, for exampletest-nextjs-saas-template.lubomirgeorgiev.com. - Preserve any existing allowed hostnames unless the user explicitly wants to remove them.
- Use Managed mode unless the user requests another mode.
- Copy the public sitekey and private secret key.
- Open
-
Set the public sitekey as a GitHub repository variable so GitHub Actions can inject it into the production build:
bashgh variable set NEXT_PUBLIC_TURNSTILE_SITE_KEY --body "$TURNSTILE_SITE_KEY" --repo OWNER/REPO gh variable list --repo OWNER/REPO
- Set the private secret key as a Worker runtime secret. Ask the user to paste the value into the secure Wrangler prompt; never print it:
bashpnpm wrangler secret put TURNSTILE_SECRET_KEY
- If a new widget was created or an existing widget secret was rotated, remind the user that any old
TURNSTILE_SECRET_KEYstops working for that widget once rotation takes effect.
STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET, and the plan price IDs:
- Ask the user whether team subscription billing is enabled before requiring Stripe values. Billing is active only when
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET, andNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYare all set (seeisBillingEnabled()insrc/flags.ts). - If enabled, set
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYand the plan price IDs (STRIPE_PRICE_PRO,STRIPE_PRICE_ENTERPRISE) as GitHub repository variables so the production build receives them. If the price IDs do not exist yet, ask the user whether the agent should runpnpm stripe:setupon their behalf or whether they prefer to run it themselves; do not run it without confirmation because it creates products and prices in their Stripe account. If they choose the manual route, give them the exact command and flags:pnpm stripe:setupneedsSTRIPE_SECRET_KEYin.env, refuses live keys unless--liveis passed (required for production), and supports--dry-runto preview. - Set
STRIPE_SECRET_KEYandSTRIPE_WEBHOOK_SECRETas Worker runtime secrets withpnpm wrangler secret put STRIPE_SECRET_KEYandpnpm wrangler secret put STRIPE_WEBHOOK_SECRET. The webhook secret comes from the Stripe webhook endpoint for/api/stripe/webhook. Do not add Stripe secret keys to GitHub Actions secrets unless the workflow is explicitly changed to sync them into Worker secrets.
Email Sending
- Derive
SITE_URL_HOSTNAMEfromsrc/constants.tsand the expected sending subdomainnotifications.<SITE_URL_HOSTNAME>. - Use Cloudflare MCP on the production zone id for
SITE_URL_HOSTNAMEto list/zones/{zone_id}/email/sending/subdomains. Confirmnotifications.<SITE_URL_HOSTNAME>exists and is enabled. Do not assume a subdomain on a different zone (for example the template's old domain) satisfies production requirements. - Read current
wrangler.jsoncemail settings:vars.EMAIL_FROM,vars.EMAIL_FROM_NAME,vars.EMAIL_REPLY_TO, andsend_email[].allowed_sender_addresses. - Ask the user to confirm the intended production email settings before any mutation. Present:
- Sending subdomain: suggest
notifications.<SITE_URL_HOSTNAME>and explicitly ask the user to confirm they are ok with it. - From address: suggest
no-reply@notifications.<SITE_URL_HOSTNAME>and explicitly ask the user to confirm they are ok with it. - From display name for
vars.EMAIL_FROM_NAME; explicitly ask the user what production sender display name to use, even ifwrangler.jsoncalready has a value. - Reply-to address for
vars.EMAIL_REPLY_TO; explicitly ask the user what production reply-to address to use, even ifwrangler.jsoncalready has a value. - Allowed sender addresses Stop if the user has not confirmed these values.
- Sending subdomain: suggest
- If the sending subdomain is missing, create it on the production zone with Cloudflare MCP (
POST /zones/{zone_id}/email/sending/subdomainswith{ "name": "notifications.<SITE_URL_HOSTNAME>" }), then use preview/fix/status endpoints until DNS is healthy. - After user confirmation, update
wrangler.jsonc:vars.EMAIL_FROMvars.EMAIL_FROM_NAMEvars.EMAIL_REPLY_TOsend_email[].allowed_sender_addressesto includeEMAIL_FROM
- Run
pnpm run cf-typegenafterwrangler.jsoncemail var changes.
GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET:
- Ask the user whether Google SSO is enabled before requiring Google OAuth values.
- If enabled, instruct the user to create or select the OAuth client manually in Google Cloud Console. The agent must not invent OAuth credentials or scrape secret values from chat/logs/history.
- Tell the user to configure the Google OAuth redirect URI for the production domain, for example
https://<SITE_URL_HOSTNAME>/sso/google/callback. - Set
GOOGLE_CLIENT_IDas a Worker runtime variable, preferably undervarsinwrangler.jsoncfor stable project config. This value is not secret, but the user should still confirm it belongs to the intended Google OAuth client. - Set
GOOGLE_CLIENT_SECRETas a Worker runtime secret withpnpm wrangler secret put GOOGLE_CLIENT_SECRET. The user must paste the secret manually into the secure Wrangler prompt; never print it in chat.
GitHub CLI Patterns
Prefer gh for repository secrets/variables. Use gh secret set from stdin or an environment variable when handling sensitive values, and do not place secrets in shell history or command output. Do not add individual NEXT_PUBLIC_* entries to .github/workflows/deploy.yml; the deploy workflow already forwards repository variables with that prefix.
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, notworker-configuration.d.ts; runpnpm run cf-typegenafter 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. - Follow
Deployment Verificationwhen deployment-related files change and time permits.
Deployment Verification
After configuration:
- Run local checks:
bashpnpm run lint pnpm run typecheck pnpm run check:vinext pnpm run build
- If deploying through GitHub Actions, monitor the workflow:
bashgh 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.


