Production domains and third-party callbacks

Switch the site URL, check OAuth, Stripe, Turnstile, and email configuration, and verify the production domain.

After completing the initial deployment to workers.dev, you can switch the site to a production domain. This article uses https://app.example.com as an example. Replace all example URLs with your own when following the steps.

The domain configuration tutorial already covers buying a domain, adding it to Cloudflare, receiving email, and configuring a file domain. This article focuses on switching the site URL and updating services that depend on it.

1. Confirm prerequisites before switching

  • The target domain has been added to the Cloudflare account that owns the deployed Worker, and its zone is available.
  • Initial deployment is complete, and the Worker and resource bindings exist.
  • The .env.production file exists, and its SAAS_SECRET matches the local project.
  • You have chosen a single production entry point, such as app.example.com or the root domain.
  • Production Turnstile configuration is ready, and you have prepared OAuth and payment callback settings.

A Cloudflare Custom Domain must belong to an available zone. An existing CNAME for the same hostname can also affect creation. If you find a conflict, identify what the old record is used for before making changes. Do not immediately delete DNS records that still serve other applications. See the official Custom Domains documentation for platform requirements.

2. Run the domain command

Run this from the application root:

npm run domain:set -- https://app.example.com

The current script:

  1. Checks the account, Worker, production environment, and matching secret values.
  2. Runs doctor:remote.
  3. Writes the target Custom Domain to wrangler.jsonc.
  4. Updates VITE_SITE_URL in .env.production.
  5. Rebuilds and deploys, synchronizing nonempty variables and secrets from that file.
  6. Checks the new domain's homepage and prints the OAuth and Stripe callback URLs.

The script preserves routes that are not Custom Domains, but replaces the Custom Domain entries in the configuration with the current target. It sets the current production entry point. Repeated runs do not keep adding more domains.

The script does not automatically update third-party platforms, R2 file domains, email service settings, or hardcoded business URLs. Check custom hosts, allowed analytics domains, and business callbacks against your own configuration.

3. Identify where the domain switch failed

If writing configuration or building fails, the script restores the local wrangler.jsonc and .env.production files to their state before this change. Once remote deployment has started, or if post-deployment health checks fail, do not assume that the domain and remote version have also been restored automatically.

In that case, check:

  • The Custom Domain and VITE_SITE_URL in local configuration.
  • The Cloudflare Worker's current domain and deployed version.
  • DNS, HTTPS, and the homepage response.
  • Whether the old site URL or proxy configuration redirects requests to another origin.

After fixing the problem, rerun the appropriate deployment operation. The domain:set command checks only the homepage. It does not run the full verify command, database migrations, or KV synchronization. If you also changed business code or content, publish those changes through the update flow first.

4. Check production callback URLs

Use the production origin in .env.production for the following URLs, without a language prefix:

ServiceURL or host to enter
GitHub OAuth Callback URLhttps://app.example.com/api/auth/oauth2/github
Google Authorized JavaScript originshttps://app.example.com
Google Authorized redirect URIshttps://app.example.com/api/auth/oauth2/google
Stripe Webhookhttps://app.example.com/api/webhooks/stripe
Turnstile allowed hostapp.example.com

Do not prefix OAuth callbacks with /zh-Hans, /en, or the sign-in page path. Do not use the payment success return page as a webhook.

GitHub and Google sign-in

Follow the earlier GitHub OAuth and Google OAuth setup, and confirm that the production Client ID, Client Secret, and callback belong to the same application.

How you retain local and production callbacks depends on the provider and application type. Do not assume every OAuth application supports multiple URLs in one field. In particular, if you created a separate application for local development, do not change only the callback while continuing to use the other application's Client ID.

If Google One Tap is enabled, test it separately on the production site in addition to regular OAuth sign-in. It depends on the browser environment and origin configuration. Successful regular sign-in does not replace this check.

Stripe webhook

If you already created a production webhook using the Stripe payments tutorial, check its URL and the current endpoint's signing secret. Do not create a duplicate endpoint.

If you have not created one, set the live STRIPE_SECRET_KEY and production VITE_SITE_URL in .env.production, then run:

npm run webhook:stripe:prod

The script checks that the secret key is in live mode, asks you to confirm the URL, events, and API version, creates a new webhook endpoint, and writes the returned signing secret to .env.production. The default event list and version are maintained in scripts/stripe/webhook.ts. Usually, keep the defaults provided by the project.

This command creates an endpoint. It does not automatically update an existing one. After creation, also run:

npm run deploy:update

This update makes the Worker use the new STRIPE_WEBHOOK_SECRET. Some events may not synchronize during the webhook switch. Once the configuration takes effect, check failed deliveries and confirm the results of retry processing.

The STRIPE_CONNECTION_ID identifies the payment connection. Keep it stable once set. A domain switch usually does not require changing it. Otherwise, the same payment platform object may be treated as data from a different connection.

Turnstile

Add app.example.com to the production widget's host list. Confirm that .env.production uses the Site Key and Secret Key for that widget.

If you update the keys before running domain:set, that command deploys them as well. If you replace the keys after connecting the domain, run deploy:update again. Updating only the platform's allowed hosts, without changing application configuration, does not require a code deployment.

Email

The email sending domain can differ from the website domain. For example, the website can use app.example.com while email is sent through the verified domain mail.example.com. The current provider must allow the configured sender address, and site links in emails must point to the production origin.

The domain:set command does not verify your Resend domain or change email receiving rules. After deployment, send actual signup verification and password reset emails, and check the sender name, delivery, and links in the body.

5. Handle www, workers.dev, and file domains

Connecting example.com does not automatically connect www.example.com. If both addresses should work, choose the production entry point and configure the other address to redirect while preserving paths and query parameters. See Handling www addresses in domain configuration.

The current template defaults to workers_dev: true. The domain:set command does not disable it automatically. Once the production domain is stable, if you no longer need this entry point, set it to false in wrangler.jsonc, then run deploy:update. Before disabling it, confirm that no sign-in callbacks, webhooks, or business calls still depend on the old URL.

If you keep workers.dev, do not also use it as a second production origin. The application handles requests according to allowed hosts and VITE_SITE_URL. Redirecting the old URL does not replace migrating callbacks in provider dashboards.

R2 file domains are configured separately. If you need public file URLs, configure publicBaseUrl and public bucket access as described in Storage and uploads. Setting publicBaseUrl to false affects only URLs generated by the application. It does not automatically disable an R2 domain already made public in Cloudflare.

6. Verify the switch

Open a new browser session and check the following in order:

  1. The homepage, documentation, and main business pages use the production HTTPS URL.
  2. Internal page links, canonical URLs, Open Graph metadata, and similar references do not point to the old domain.
  3. Password sign-in and enabled OAuth sign-in methods return to the production site.
  4. Turnstile completes as expected, with no domain mismatch errors in network requests.
  5. Verification and reset links in emails use the production domain.
  6. The Stripe endpoint receives the expected events, and the Worker verifies signatures and updates business state.
  7. Visits to old entry points behave as expected, without redirect loops.

After changing domains, do not assume the browser will automatically send sign-in cookies from the old domain to the new one. Sign in again to verify the flow.

Continue to Launch verification and routine maintenance.