Cloudflare

Learn how to deploy your TurboStarter app to Cloudflare Workers.

For the complete documentation index, see llms.txt. Prefer markdown by appending .md to documentation URLs or sending Accept: text/markdown.

Cloudflare Workers can run your TurboStarter web app close to your users on Cloudflare's global network. TurboStarter uses OpenNext for Cloudflare to build the Next.js app into a Worker, Wrangler to preview and deploy it, Hyperdrive for database connections, and R2 for Next.js incremental cache.

This guide focuses on the most convenient path: generate the Cloudflare files, create the required Cloudflare resources, preview the app locally in the Workers runtime, then deploy.

Prerequisites

Before deploying, make sure you have:

Cloudflare Workers have script size limits. Wrangler prints the compressed upload size during deployment, so check that output if the upload is rejected.

Cloudflare Workers & Pages dashboard

Generate the Cloudflare setup

Run the Cloudflare generator from the repository root:

pnpm turbo gen cloudflare

The generator asks for:

  • Worker name - the Cloudflare Worker name, usually your product slug
  • R2 bucket name - the bucket used by OpenNext for incremental cache
  • Hyperdrive config id - paste this after you create Hyperdrive in the next step
  • Local database URL - used by Wrangler preview
  • App URL - used by local Cloudflare preview
  • Wrangler compatibility date - keep the default unless you have a reason to pin it

It creates and updates the files needed by the web app:

  • apps/web/wrangler.jsonc
  • apps/web/open-next.config.ts
  • apps/web/scripts/cf-build.mjs
  • apps/web/.dev.vars.example
  • apps/web/next.config.ts
  • apps/web/package.json
  • apps/web/middleware.ts
  • packages/db/src/server.ts
  • turbo.json

It also adds scripts like cf:build, cf:preview, cf:deploy, cf:upload, and cf:typegen to the web package.

Prefer doing it manually?

You can add the same files yourself. Use the generated setup as the source of truth: wrangler.jsonc points to .open-next/worker.js, enables nodejs_compat, binds ASSETS, IMAGES, NEXT_INC_CACHE_R2_BUCKET, and HYPERDRIVE, while open-next.config.ts enables the R2 incremental cache override.

Create Hyperdrive

Cloudflare Workers run globally, while most Postgres databases live in one region. Hyperdrive keeps pooled connections inside Cloudflare's network and exposes a Worker binding called HYPERDRIVE.

Create a Hyperdrive configuration with your production database URL:

pnpm --filter web exec wrangler hyperdrive create <name> --connection-string="$DATABASE_URL"

Copy the returned id into apps/web/wrangler.jsonc:

apps/web/wrangler.jsonc
{
  "hyperdrive": [
    {
      "binding": "HYPERDRIVE",
      "id": "<your-hyperdrive-id>",
      "localConnectionString": "postgresql://turbostarter:turbostarter@localhost:5432/core"
    }
  ]
}

Keep localConnectionString pointed at your local database. Wrangler uses it when you run cf:preview.

Hyperdrive mechanics

Create the R2 cache bucket

The generated open-next.config.ts configures OpenNext to use R2 for incremental cache. Create the bucket with the same name you entered in the generator:

pnpm --filter web exec wrangler r2 bucket create <bucket-name>

Then verify the binding in apps/web/wrangler.jsonc:

apps/web/wrangler.jsonc
{
  "r2_buckets": [
    {
      "binding": "NEXT_INC_CACHE_R2_BUCKET",
      "bucket_name": "<bucket-name>"
    }
  ]
}

If you are not using ISR or cached server data yet, you can still keep this bucket configured. It gives your app the right production shape before you need it.

Using R2 for app uploads

This bucket is for OpenNext's incremental cache. If your app stores user uploads, exports, avatars, or generated files, create a separate Cloudflare R2 bucket and wire it through the storage package. Keeping cache and user files separate makes permissions, lifecycle rules, and cleanup much easier.

Configure environment variables

Cloudflare needs two kinds of variables:

  • Build-time variables - available when cf:build runs, especially NEXT_PUBLIC_ values and anything used by static generation
  • Runtime variables and secrets - available to the deployed Worker

For local preview, copy the generated example file:

cp apps/web/.dev.vars.example apps/web/.dev.vars

Keep your normal local values in .env.local and apps/web/.env.local as described in environment variables. Use .dev.vars only for values Wrangler needs during Worker preview, such as DATABASE_URL and URL.

For production, add the same variables you use for other deployments:

Production variables
URL="https://example.com"
BETTER_AUTH_URL="https://example.com"
NEXT_PUBLIC_URL="https://example.com"
DATABASE_URL="postgresql://..."
BETTER_AUTH_SECRET="..."

You can set them in the Cloudflare dashboard or with Wrangler:

pnpm --filter web exec wrangler secret put DATABASE_URL
pnpm --filter web exec wrangler secret put BETTER_AUTH_SECRET

The generated wrangler.jsonc includes keep_vars: true, so deployments will not remove variables you manage in the Cloudflare dashboard.

Use separate staging and production values

If you deploy multiple environments, keep the same variable names and change only the values. The multiple environments recipe shows the recommended structure.

Cloudflare variables

Generate Cloudflare types

After wrangler.jsonc has a real Hyperdrive id and R2 bucket name, generate Worker binding types:

pnpm --filter web cf:typegen

This creates apps/web/cloudflare-env.d.ts. The generator also adds that file to the web app's TypeScript config.

Preview locally

Run the app in the same Workers runtime it will use in production:

pnpm --filter web cf:preview

Test the paths that depend on external services:

  • sign in and sign out
  • organization creation
  • database reads and writes
  • billing checkout and webhooks
  • email sending
  • file uploads, if your app uses storage

Preview before every first deploy

pnpm dev is still the fastest daily development command. cf:preview is the production-shape check that catches Worker, Hyperdrive, and environment issues before your users do.

Deploy

Deploy the Worker from your local machine:

pnpm --filter web cf:deploy

This runs the custom Cloudflare build script, transforms the Next.js output with OpenNext, uploads the Worker, and deploys it to Cloudflare.

If you want to upload a version without immediately routing traffic to it, use:

pnpm --filter web cf:upload

After deployment, open the Worker URL and update your production URL, NEXT_PUBLIC_URL, BETTER_AUTH_URL, OAuth callbacks, and billing webhooks to use the final domain.

Cloudflare deployment

Add a custom domain

In the Cloudflare dashboard, open your Worker and add a custom domain or route for your production host.

After the domain is active:

  • update URL, NEXT_PUBLIC_URL, and BETTER_AUTH_URL
  • update OAuth redirect URLs in each provider
  • update billing webhook URLs to /api/billing/webhook
  • redeploy with pnpm --filter web cf:deploy

Custom domain

Optional Cloudflare services

You do not need these services for the first deployment, but they are useful once your app grows beyond the default Worker, Hyperdrive, and R2 cache setup.

Start small

Keep Postgres as the main TurboStarter database unless you intentionally redesign that layer. Services like KV, D1, Durable Objects, and Queues are best added for specific workloads, not as replacements for the default app database on day one.

Useful commands

# Build the OpenNext Worker output
pnpm --filter web cf:build

# Preview locally in the Workers runtime
pnpm --filter web cf:preview

# Deploy to Cloudflare
pnpm --filter web cf:deploy

# Upload a new Worker version without deploying traffic immediately
pnpm --filter web cf:upload

# Regenerate Cloudflare binding types
pnpm --filter web cf:typegen

Troubleshooting

Build fails because an environment variable is missing

TurboStarter validates environment variables during build. Make sure build-time values exist locally before cf:build, and runtime values exist in Cloudflare before the deployed Worker starts.

For NEXT_PUBLIC_ values, remember that Next.js inlines them during the build.

Hyperdrive binding is missing

Check that apps/web/wrangler.jsonc includes a hyperdrive entry with binding: "HYPERDRIVE" and a real id. Then rerun:

pnpm --filter web cf:typegen
pnpm --filter web cf:preview

Upload fails because the Worker is too large

Wrangler prints the compressed Worker size during upload. If it exceeds your Cloudflare plan limit, remove unused server dependencies, avoid importing large packages into server routes, or upgrade the Workers plan.

A package expects unsupported Node.js behavior

The generated wrangler.jsonc enables nodejs_compat, which is required for many Next.js and database use cases on Workers. Some Node.js APIs are still only partially supported, so replace Node-only libraries with HTTP/fetch-based providers when needed.

A route uses the Edge runtime

OpenNext for Cloudflare is designed around the Next.js Node.js runtime on Workers. If you added export const runtime = "edge" to a route, remove it and preview again.

Next steps

Official references

How is this guide?

Last updated on

On this page

Ship your startup everywhere. In minutes.Try TurboStarter