Skip to content

Deploy to Cloudflare Workers

This guide deploys a SvelteKit appa Next.js app with Auth UI to a Cloudflare Worker, built from your Git repository by Workers Builds. npx convex deploy in the build command deploys your Convex functions in the same build. Production builds deploy to your production Convex deployment. Every other branch becomes a Worker Preview with a Convex preview deployment of its own, with a fresh, empty database.

The Worker runs your app with vinext, Cloudflare’s implementation of the Next.js API on Vite. vinext is in beta, so check your app with it before you move.

1. Add the adapter

The vite.config.ts that Auth UI installs uses @sveltejs/adapter-auto. Install the Cloudflare adapter and Wrangler, Cloudflare’s CLI, which Workers Builds runs from your project to deploy:

vite.config.ts

Worker Previews need Wrangler 4.135.0 or later.

2. Add a Wrangler configuration

Create wrangler.jsonc next to package.json, with my-app as your Worker name:

wrangler.jsonc
  • main and assets point at the adapter’s output. The adapter reads them from this file.
  • compatibility_date is the date of the Workers runtime behavior you build against. Set it to today. Node.js APIs are on by default, so no compatibility flag is needed.
  • preview_urls serves each Preview on workers.dev.
  • previews holds the settings Previews start with, because they don’t inherit production’s. npx wrangler preview requires the block. The app reads its Convex URLs at build time and needs no runtime variables, so it can stay empty.

Commit and push both changes.

1. Check your app

Use Node.js 24.22 or later. vinext deploys with Cloudflare’s cf CLI, which needs it, and Workers Builds’ build image already has it. From your project root, run:

It lists the Next.js APIs, config options and libraries your app uses, and how far vinext supports each. Fix every line it reports as an issue. If your app needs something vinext doesn’t support, deploy it with OpenNext instead, which runs the output of next build on Workers. This guide doesn’t cover it.

2. Set up vinext

Run vinext init with my-app as your Worker name:

It installs vinext, Vite, @vinext/cloudflare, Cloudflare’s Vite plugin and the cf CLI. It adds "type": "module" and the dev:vinext, build:vinext and deploy:vinext scripts to package.json, creates vite.config.ts and cloudflare.config.ts, and adds vinext’s output folders to .gitignore. next dev keeps working. The two flags leave out the CDN cache and Cloudflare Images. Without them, init asks.

With pnpm, the install doesn’t finish until you allow the install script of workerd, the Workers runtime the Vite plugin runs. Run pnpm approve-builds, select workerd, then run pnpm install. Commit the setting it writes, because Workers Builds installs with it too.

The Worker’s name is in cloudflare.config.ts. Add workersDev and previewUrls:

cloudflare.config.ts
  • compatibilityDate is the date init ran.
  • workersDev serves production on workers.dev, and previewUrls serves each Preview there. A production deploy applies the settings in this file, so with both set it doesn’t switch them off.

Leave the rest as init generated it.

Two changes to the files Auth UI installed:

  • In src/app/globals.css, the @plugin '@tailwindcss/forms'; line goes after the last @import. Vite inlines the imports, and with @plugin before them the page loses the Auth UI theme.
  • vinext builds into dist/, .vinext/ and .cloudflare/. Add them to the ignores in eslint.config.mjs, or eslint . lints the build output.

Commit and push the changes.

3. Create the Worker

In the Cloudflare dashboard, go to Workers & Pages and click Create application. Choose GitHub or GitLab and select your repository. Then configure the project:

  • Project name: the name from wrangler.jsonccloudflare.config.ts, for example my-app. The build fails if they differ.
  • Build command: npx convex deploy --cmd='vite build'
  • Deploy command: npx wrangler deploy, the default.npx vinext-cloudflare deploy --skip-build
  • Root directory: leave it empty, or set the folder of your app in a monorepo.

Don’t add PUBLIC_CONVEX_URL or PUBLIC_CONVEX_SITE_URLNEXT_PUBLIC_CONVEX_URL or NEXT_PUBLIC_CONVEX_SITE_URL. npx convex deploy sets them for vite build to the URLs of the Convex deployment it deploys to.If you ever set them yourself, for example for a build that doesn’t deploy Convex, keep the NEXT_PUBLIC_ prefix: Auth UI’s Next.js code reads no other name.

Click Save and Deploy. The first build fails because CONVEX_DEPLOY_KEY isn’t set yet.

4. Check the branch settings

In your Worker, go to Settings, then Build, then Branch control:

  • Production branch: main. Builds of this branch run the deploy command.
  • Enable Preview Builds: on. Builds of every other branch run the Preview command, npx wrangler preview. Set it on the Previews Base tab to npx vinext-cloudflare previews deploy --skip-build.

The Convex CLI treats a Workers Builds build as production only on a branch named main, and refuses a production deploy key on any other branch. If your production branch has another name, add --check-build-environment=disable to the build command.

A new Worker takes its Preview URLs setting from your configuration only on its first production deploy, so until then a Preview has no URL. In your Worker, go to Settings, then Domains & Routes, and turn on Preview URLs.

A Worker connected to Workers Builds before Worker Previews existed keeps the older preview model, which runs npx wrangler versions upload with production’s settings, until you switch it once in the same settings.

5. Add the deploy keys in Cloudflare

Generate a production deploy key and a preview deploy key in the Convex dashboard. In your Worker, go to Settings, then Build. The build settings have a Production tab and a Previews Base tab, each with its own Variables and secrets. Add CONVEX_DEPLOY_KEY to each, with Secret as the type:

The production key deploys only from a build of a branch named main, as step 4 explains. These are build variables, which only the build sees. Don’t add the key under Settings, Variables & Secrets: those are runtime variables of the deployed Worker.

That’s the only variable Cloudflare needs. Everything else lives in Convex.

Add one more variable to Previews Base, with Text as the type:

vite build writes a Preview build only when it’s set, and the Preview command deploys nothing else. Keep it off the Production tab: the deploy command refuses a Preview build. Everything else lives in Convex.

6. Set the Convex environment variables

Convex functions read their variables from the deployment they run on, so set them in Convex in two places:

  • Your production deployment’s settings, for production.
  • Project Settings, Environment Variable Defaults, for every new development and preview deployment. Convex keeps a separate set of defaults for each deployment type. Each preview build creates a new deployment, which copies the preview defaults when it’s created.

Production:

Defaults for development deployments, with my-app as your Worker name and acme as your account’s workers.dev subdomain:

Defaults for preview deployments, the same plus the protocol:

From the CLI, npx convex env default set NAME value --type preview sets a preview default and --type dev a development one.

Use a different BETTER_AUTH_SECRET for production than for development and previews. Without a secret in the defaults, a preview deployment has none.

Allowed hosts

BETTER_AUTH_ALLOWED_HOSTS is a comma-separated list of hosts, and * matches any characters, dots included. Cloudflare serves a Preview at <preview>-<worker>.<subdomain>.workers.dev, where the preview name comes from the branch, and each of its deployments at <deployment-id>-<worker>.<subdomain>.workers.dev. So *-my-app.acme.workers.dev matches the previews of one Worker. Don’t use *.workers.dev: it trusts every Worker on Cloudflare. For production, list your exact domains, or my-app.acme.workers.dev if you serve production on workers.dev, and add *.acme.com only if every subdomain should be trusted. See Better Auth’s Dynamic Base URL for the pattern rules.

BETTER_AUTH_FALLBACK_URL is used only when Better Auth runs without an incoming request, as in Convex queries, mutations, triggers, scheduled functions and device authorization. All previews share the one value from the defaults, so links built without a request point there. Previews still need it: without it, pages that render a signed-in user on the server fail.

Without BETTER_AUTH_BASE_URL_PROTOCOL, cookies get the __Secure- prefix and the Secure attribute only when BETTER_AUTH_FALLBACK_URL is an https URL. Production, with https://acme.com, needs nothing more. Previews are served over HTTPS but share the http://localhost fallback URL, so the preview defaults set BETTER_AUTH_BASE_URL_PROTOCOL="https", or Preview cookies go without Secure. Leave it out of the development defaults, because local development runs on http. Switching a deployment to Secure cookies renames them to __Secure-…, so people signed in before sign in once more.

Feature variables

Copy the variables of the features you turned on to both the production deployment and the defaults. The feature pages explain each one.

VariableNeeded for
RESEND_API_KEY, EMAIL_SEND_FROMsendEmails: true. Every email fails without EMAIL_SEND_FROM.
RESEND_WEBHOOK_SECRETResend delivery events. The webhook URL is https://<deployment>.convex.site/resend-webhook, so each deployment needs its own webhook in Resend.
BRAND_LOGO_URLA logo in the emails (optional)
REOON_EMAIL_VERIFIER_TOKENvalidateEmails: true
DEVICE_AUTHORIZATION_CLIENT_IDdeviceAuthorization: true
<PROVIDER>_CLIENT_ID, <PROVIDER>_CLIENT_SECRETEach social provider, for example GITHUB_CLIENT_ID. TikTok uses TIKTOK_CLIENT_KEY instead of a client id.
ADMIN_USER_IDSadmin: true. Set it per deployment, not in the defaults.

ADMIN_USER_IDS holds Better Auth user ids, and each preview deployment starts with an empty database, so an id from development or production matches nobody there. Sign up on the preview, then set the variable on that deployment in the Convex dashboard.

See Configure features for the flags and Sending emails for the Resend setup.

7. Deploy

Push a commit, or retry the failed build from Deployments, View build history. Cloudflare builds again on every push: your production branch to production, other branches to their Previews. On GitHub and GitLab it posts each Preview’s URL on the pull request. You also find it in your Worker’s Previews section.

How preview deployments work

When Cloudflare builds a branch other than main:

  1. npx convex deploy reads the preview CONVEX_DEPLOY_KEY from the Previews Base variables.
  2. It creates or updates a Convex preview deployment named after the branch in WORKERS_CI_BRANCH. A new deployment copies the environment variable defaults.
  3. It runs vite build with PUBLIC_CONVEX_URL and PUBLIC_CONVEX_SITE_URLNEXT_PUBLIC_CONVEX_URL and NEXT_PUBLIC_CONVEX_SITE_URL set to that preview deployment, then deploys your Convex functions to it.
  4. The Preview command, npx wrangler previewnpx vinext-cloudflare previews deploy --skip-build, uploads the build as the branch’s Worker Preview. The preview site and its Better Auth requests use that Convex preview deployment.

Moving an existing app from Cloudflare Pages? Follow Cloudflare’s migration guide, then replace *.<project>.pages.dev in BETTER_AUTH_ALLOWED_HOSTS with the workers.dev pattern above.