Deployment
Provision Cloudflare resources, configure production bindings and secrets, migrate D1 and deploy the web, admin and platform Workers of the Next.js + Cloudflare D1 starter.
The kit deploys as three Cloudflare Workers: the OpenNext web Worker, the OpenNext admin Worker and the platform Worker. All three need the same application D1 database, and the two Next.js Workers also need the OpenNext cache resources. The shared Cloudflare deployment guide has the platform-generic steps; this page lists what is specific to this kit.
Plan
The live demo runs on Workers Paid. During testing, the Better Auth password work exceeded the Free plan's CPU limit and produced intermittent errors. Check your plan if login fails only sometimes after deploying. See troubleshooting.
1. Create the Cloudflare resources
Create these in your account, using the names you want (the checked-in names are the local defaults):
| Resource | Default name | Used by |
|---|---|---|
| D1 database (application) | nextbase-app | all three Workers, binding APP_DB |
| D1 database (OpenNext tag cache) | nextbase-next-cache | web and admin, binding NEXT_TAG_CACHE_D1 |
| R2 bucket (media) | nextbase-media | all three Workers, binding MEDIA_BUCKET |
| R2 bucket (OpenNext incremental cache) | nextbase-next-cache | web and admin, binding NEXT_INC_CACHE_R2_BUCKET |
| Queue | nextbase-jobs | platform producer and consumer |
| Queue (dead letter) | nextbase-jobs-dlq | platform consumer |
| Queue (terminal) | nextbase-jobs-dlq-persistence-failures | no consumer; see the README runbook |
The queue commands from the repository are:
pnpm --filter @nextbase/platform-worker exec wrangler queues create nextbase-jobs --message-retention-period-secs 86400
pnpm --filter @nextbase/platform-worker exec wrangler queues create nextbase-jobs-dlq --message-retention-period-secs 86400
pnpm --filter @nextbase/platform-worker exec wrangler queues create nextbase-jobs-dlq-persistence-failures --message-retention-period-secs 86400Durable Objects (DOQueueHandler, RealtimeChannel) and the two rate limiters are created from the wrangler.jsonc declarations on first deploy. The repository doesn't include commands for creating the D1 databases or R2 buckets; use wrangler d1 create and wrangler r2 bucket create, or the dashboard.
2. Edit the production environment in the three Wrangler files
The top-level D1 IDs (00000000-...) are placeholders for local work. Each wrangler.jsonc (apps/web, apps/admin, apps/platform-worker) also has an env.production block that repeats every binding with the demo's real resource IDs, names and account_id. Replace those values with your own; keep the binding names unchanged. The two Next.js production blocks also:
- give each Worker a distinct name (
nextbase-cloudflare-web-production,nextbase-cloudflare-admin-production) and pointWORKER_SELF_REFERENCEat that name, - set
BETTER_AUTH_URLandNEXT_PUBLIC_SITE_URLto that Worker's public origin (the web origin on web, the admin origin on admin), - cross-reference the other app:
NEXT_PUBLIC_ADMIN_APP_URLon web,NEXT_PUBLIC_WEB_APP_URLandADMIN_APP: "true"on admin, - set
ADMIN_EMAILto the address that should become the first admin, - set
USE_LOCAL_EMAILto"false"andMAILPIT_API_URLto"".
The platform Worker's production block mirrors its bindings and vars, and adds MAIL_FROM with a verified sender such as Your App <no-reply@your-domain>.
Run pnpm cf:typegen:check afterwards so the generated binding types still match.
3. Set the Worker secrets
From each app directory, add the secrets for the production environment:
cd apps/web
pnpm exec wrangler secret put BETTER_AUTH_SECRET --env production
pnpm exec wrangler secret put REALTIME_TOKEN_SECRET --env production
pnpm exec wrangler secret put REVALIDATE_SECRET --env production
pnpm exec wrangler secret put STRIPE_SECRET_KEY --env production
pnpm exec wrangler secret put STRIPE_WEBHOOK_SECRET --env production
# optional: RESEND_API_KEY, OPENAI_API_KEY, GOOGLE_*, GITHUB_*, TWITTER_*
cd ../admin
pnpm exec wrangler secret put BETTER_AUTH_SECRET --env production # a different value from web
pnpm exec wrangler secret put REALTIME_TOKEN_SECRET --env production
pnpm exec wrangler secret put REVALIDATE_SECRET --env production
pnpm exec wrangler secret put STRIPE_SECRET_KEY --env production
pnpm exec wrangler secret put STRIPE_WEBHOOK_SECRET --env production
# optional: RESEND_API_KEY, OPENAI_API_KEY, GOOGLE_*, GITHUB_*, TWITTER_*
cd ../platform-worker
pnpm exec wrangler secret put REALTIME_TOKEN_SECRET --env production
pnpm exec wrangler secret put INTERNAL_SERVICE_SECRET --env production
pnpm exec wrangler secret put RESEND_API_KEY --env production
pnpm exec wrangler secret put STRIPE_SECRET_KEY --env productionREALTIME_TOKEN_SECRET must be identical on all three Workers and at least 32 characters. INTERNAL_SERVICE_SECRET must be a separate value of at least 32 characters. The admin Worker should get its own BETTER_AUTH_SECRET; the admin uses a different cookie prefix, so nothing needs to be shared. The full list is in environment variables.
4. Apply the migrations
Both D1 databases need their migrations before the first request:
pnpm db:migrate:productionThis runs wrangler d1 migrations apply --remote --env production for APP_DB and NEXT_TAG_CACHE_D1 using apps/web/wrangler.jsonc. The admin Worker shares those databases and owns no migrations. pnpm db:migrate:remote runs the same two commands without --env production, so it only works if your top-level bindings already point at the production databases. The application migrations include 0010_famous_groot.sql, which creates mcp_records; both Next.js Workers need it before serving OAuth connections.
5. Build and deploy
Deploy the platform Worker first so the PLATFORM service binding exists when the Next.js Workers go live. The root script does that in order:
NEXT_PUBLIC_SITE_URL=https://your-domain pnpm cf:deploy:productioncf:deploy:production runs wrangler deploy --env production in apps/platform-worker, then cf:deploy:production in apps/web, which builds with opennextjs-cloudflare build --env production and deploys. NEXT_PUBLIC_SITE_URL has to be present in the shell because it is compiled into the browser bundle; the web build script stops with Set NEXT_PUBLIC_SITE_URL to the public origin of this deployment when it is missing, and no default is baked in. A build made with the default environment contains the placeholder D1 IDs and the localhost URL, so always rebuild with --env production.
Then deploy the admin Worker:
pnpm cf:deploy:admin:productionThe admin build script in apps/admin/package.json sets NEXT_PUBLIC_SITE_URL and NEXT_PUBLIC_WEB_APP_URL inline, and the checked-in values are the demo's https://nextjs-d1-admin.usenextbase.com and https://nextjs-d1.usenextbase.com. Edit that cf:build:production script to your own admin and web origins before running it.
pnpm cf:deploy and pnpm cf:deploy:admin are the same flows without --env production; use them only when your top-level configuration is the production one. pnpm cf:build builds all three Workers without deploying.
GitHub Actions. .github/workflows/deploy.yml is a manual workflow_dispatch workflow with an app input (web or admin). It installs, runs typecheck, tests and lint, then runs pnpm --filter <app> cf:deploy:production with CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID from repository secrets and NEXT_PUBLIC_SITE_URL set in the workflow file to the demo URL. Change that value to your web origin. It does not deploy the platform Worker; run that from your machine or add a step.
6. Configure the external services
- Auth callbacks: register
https://your-domain/api/auth/callback/google,/githuband/twitterfor the providers you enabled, and the same paths on the admin origin if administrators should use a provider there. Each Worker trusts only its ownNEXT_PUBLIC_SITE_URL, so the public origins and the Worker vars must match exactly. - Stripe: point the webhook endpoint at
https://your-domain/api/stripe/webhookson the web Worker and store its signing secret inSTRIPE_WEBHOOK_SECRET. The admin Worker reads billing records from D1 and queues product synchronization to the platform Worker, which keeps the Stripe secret. - Email: set
RESEND_API_KEYon the platform Worker andMAIL_FROMto a sender on a verified Resend domain. Auth emails are queued in the D1 outbox and dispatched by the platform Worker's cron every minute. See emails. - Custom domains: the demo serves
https://nextjs-d1.usenextbase.comandhttps://nextjs-d1-admin.usenextbase.comthrough two small Cloudflare Pages projects,infrastructure/demo-domainandinfra/admin-domain, that forward to the production Workers with anAPPservice binding. Deploy the web proxy with the command ininfrastructure/demo-domain/README.mdand the admin proxy withpnpm --filter admin cf:deploy:domain, then point each DNS CNAME at the Pages hostname (nextjs-d1-starter-domain.pages.devandnextjs-d1-admin-domain.pages.devfor the demo). Workers custom domains on the Workers themselves work as well; whichever you pick, keep each Worker'sBETTER_AUTH_URLandNEXT_PUBLIC_SITE_URLequal to its public address. - Agent connections: no extra variable. The User MCP is
https://your-domain/mcp/useron the web Worker and the Admin MCP ishttps://your-admin-domain/mcp/adminon the admin Worker, each derived from that Worker'sNEXT_PUBLIC_SITE_URL.
7. Smoke checks
- Open the web site, sign up with the
ADMIN_EMAILaddress and open the verification email. Then open the admin origin, sign in with that account and confirm you land in/app-admin. - Sign up with a second address on the web site, verify, create a workspace and a project.
- Request
https://your-domain/mcp/userandhttps://your-admin-domain/mcp/adminwithout a token and confirm a401with aWWW-Authenticateheader, then check/.well-known/oauth-authorization-server/oauthreturns JSON on both origins. - Connect an agent to
/mcp/useron the web origin, approve one workspace and list projects. Try/mcp/adminon the admin origin with the admin account. - From
/app-admin/users, impersonate the second account and confirm you are redirected to the web origin with the impersonation banner, and that the banner's link returns you to the admin app. - Check
https://<platform-worker>.workers.dev/healthand watch the Worker logs foroutboxor CPU-limit errors during the first few sign-ups. - If you use Stripe, trigger a test checkout and confirm the webhook projection lands in
/app-admin/payment-gatewayon the admin origin.
If something fails, troubleshooting covers the common D1, login and MCP cases.