Deployment
Provision Cloudflare resources, align the web, admin and jobs Wrangler configs, apply D1 migrations and deploy the three Workers.
This kit deploys as three Cloudflare Workers that share one D1 database: the web Worker (apps/web), the admin Worker (apps/admin) and the jobs Worker (apps/jobs). The repository ships preview and production environments in all three Wrangler files. The platform-generic steps are in deploy on Cloudflare; this page covers what is specific to this repository.
The repository's docs/operations.md is the runbook for migrations, outbox recovery, webhooks, rollback and secret rotation, and the "Separate administrator app" section of README.md covers the admin Worker. Read both before your first production release.
Create the Cloudflare resources
Create separate resources for preview and production. Never point preview bindings at production resources.
pnpm exec wrangler d1 create nextbase-prod
pnpm exec wrangler r2 bucket create nextbase-public-media
pnpm exec wrangler r2 bucket create nextbase-private-media
pnpm exec wrangler queues create nextbase-jobs
pnpm exec wrangler queues create nextbase-jobs-dlqRepeat with the resource names declared in the env.preview and env.production blocks: nextbase-preview-* and nextbase-production-* for the R2 buckets, queues and dead-letter queues. Pick your own D1 database names; the shipped values are placeholders. The admin Worker creates nothing new: it binds the same database, buckets and queue.
Durable Objects need no provisioning. Wrangler creates the RealtimeRoom and AuthRateLimiter namespaces from the migrations block in apps/web/wrangler.jsonc on first deploy. Keep both migration tags (v1, v2) when you rename Workers or add environments.
Update the three Wrangler files and keep them aligned
Edit apps/web/wrangler.jsonc, apps/admin/wrangler.jsonc and apps/jobs/wrangler.jsonc together. For each environment:
| Setting | Web Worker | Admin Worker | Jobs Worker | Must match |
|---|---|---|---|---|
name | nextbase-tanstack-d1-preview / -production | nextbase-tanstack-d1-admin-preview / -production | nextbase-jobs-preview / -production | Every durable_objects.bindings[].script_name in the admin and jobs files must equal the web Worker's name for the same environment. |
d1_databases[].binding | APP_DB | APP_DB | APP_DB | Yes, and keep the name APP_DB. |
d1_databases[].database_name and database_id | your database | your database | your database | Yes. Replace the 11111111-... placeholder and the demo's production ID with the real ID from wrangler d1 create. |
d1_databases[].migrations_dir | ../database/migrations | same | same | Yes. |
r2_buckets | PUBLIC_MEDIA, PRIVATE_MEDIA | same bindings | same bindings | Same bucket names. |
queues.producers[].queue | the environment's queue | same queue | same queue | Yes. Only the jobs file lists the consumers and the -dlq queue. |
vars.SITE_URL | your public web origin | your admin origin | the web origin | Web and jobs identical; admin is its own origin. |
vars.ADMIN_APP_URL | the admin origin | Must equal the admin SITE_URL. | ||
vars.WEB_APP_URL | the web origin | Must equal the web SITE_URL. Never set it to the admin origin. | ||
vars.ADMIN_APP | true | Keep it true on the admin Worker only. | ||
vars.ADMIN_EMAIL | a real mailbox | same | same | Yes. |
vars.API_CORS_ORIGINS, vars.UNKEY_API_ID, vars.APP_NAME | web | admin | Set on web; the admin file carries the same keys. | |
vars.MAIL_FROM | optional verified sender | Jobs only. |
Only the web Worker defines the RealtimeRoom and AuthRateLimiter classes. The admin Worker reaches both and the jobs Worker reaches REALTIME through script_name, so a mismatch there breaks realtime invalidations or admin rate limiting even though the deploy succeeds. Deploy the web Worker before the other two so the referenced script exists.
All three Workers read the same database
If the APP_DB entries point at different databases, the web app will enqueue emails and jobs that the jobs Worker never sees, verification emails will never be delivered, and the admin console will show a different set of users. Check the database_id in all three files for the same environment before deploying.
Set the secrets on the Worker that uses them
pnpm --filter @nextbase/web exec wrangler secret put BETTER_AUTH_SECRET --env preview
pnpm --filter @nextbase/web exec wrangler secret put ANALYTICS_PSEUDONYM_SECRET --env preview
pnpm --filter @nextbase/web exec wrangler secret put STRIPE_SECRET_KEY --env preview
pnpm --filter @nextbase/web exec wrangler secret put STRIPE_WEBHOOK_SECRET --env preview
pnpm --filter @nextbase/web exec wrangler secret put OPENAI_API_KEY --env preview
pnpm --filter @nextbase/web exec wrangler secret put UNKEY_ROOT_KEY --env preview
pnpm --filter @nextbase/admin exec wrangler secret put BETTER_AUTH_SECRET --env preview
pnpm --filter @nextbase/jobs exec wrangler secret put STRIPE_SECRET_KEY --env preview
pnpm --filter @nextbase/jobs exec wrangler secret put RESEND_API_KEY --env preview
pnpm --filter @nextbase/jobs exec wrangler secret put UNKEY_ROOT_KEY --env preview
pnpm --filter @nextbase/jobs exec wrangler secret put CLOUDFLARE_API_TOKEN --env preview
pnpm --filter @nextbase/jobs exec wrangler secret put CLOUDFLARE_ZONE_ID --env previewRepeat with --env production. BETTER_AUTH_SECRET is mandatory on the web and admin Workers: both throw at startup on a non-localhost SITE_URL without it, and the admin value must be a different secret from the web value. Add the social provider client IDs and secrets the same way if you use them. RESEND_API_KEY and the cache-purge credentials belong only on the jobs Worker. The full list is on the environment variables page.
Apply the D1 migrations
Apply the schema before the first application deploy, and again whenever a release adds a migration:
pnpm db:migrate:preview
pnpm db:migrate:productionBoth scripts run wrangler d1 migrations apply APP_DB --remote with the matching --env against apps/web/wrangler.jsonc. pnpm db:migrate:remote is an alias for the preview command. The run includes 0025_user_admin_mcp.sql (the mcp_records table), which both the web and admin Workers need before they serve OAuth connections.
Before a destructive migration, record a D1 Time Travel bookmark and the release SHA. Never reverse an applied migration; ship a forward repair migration instead.
Build and deploy the Workers
pnpm cf:deploy:preview
pnpm cf:deploy:admin:preview
pnpm cf:deploy:production
pnpm cf:deploy:admin:productioncf:deploy:<env> sets CLOUDFLARE_ENV to the environment, runs the React Router build for @nextbase/web, deploys the web Worker with wrangler deploy --env, then deploys the jobs Worker with wrangler deploy --env. cf:deploy:admin:<env> does the same for @nextbase/admin alone. Run the web script first so the Durable Object classes exist for the admin and jobs bindings. The CLOUDFLARE_ENV value matters: the Vite build generates the redirected Wrangler configuration from it. Running wrangler deploy --env production after a build without that variable can keep the local placeholder database ID, so always use the root scripts.
The repository also has a manual GitHub Actions workflow, .github/workflows/deploy.yml. Trigger it with workflow_dispatch, choose the environment (preview or production) and the app (web or admin); it runs pnpm verify and pnpm test:integration, applies db:migrate:<env> only for the web app, then runs the matching cf:deploy:* script. It needs CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as secrets on the GitHub deployment environment of the same name.
Point auth, email and MCP at the public origins
SITE_URLin the web and jobs Wrangler files must be the address users visit, andSITE_URLin the admin file must be the admin address. Better Auth uses each asbaseURLand trusted origin, so cookies, verification links and OAuth callbacks break if they differ.- Set web
ADMIN_APP_URLand adminWEB_APP_URLto each other's origin. Impersonation hands the browser from the admin origin toWEB_APP_URL+/auth/impersonatewith a one-minute, single-use token and returns toADMIN_APP_URL+/app-admin/users. - Social providers: register
SITE_URL+/api/auth/callback/<provider>with each provider you enabled, once per origin that should offer the button. - Email sender: the jobs handler sends from
MAIL_FROMwhen set, otherwise fromADMIN_EMAIL, through Resend. Verify that domain in Resend. See emails with Resend. - Stripe: point the webhook endpoint at the web
SITE_URL+/api/stripe/webhooksand subscribe to the event types listed indocs/operations.md. - MCP: the User MCP is the web
SITE_URL+/mcp/userand the Admin MCP is the adminSITE_URL+/mcp/admin. There is no separateMCP_APP_URLin this kit; each Worker's origin comes from its ownSITE_URL. - Public API: set
API_CORS_ORIGINSto the exact origins allowed to call/api/v1/me. - Custom domains: the demo attaches
tanstack-d1.usenextbase.comandtanstack-d1-admin.usenextbase.comthrough two small Cloudflare Pages proxies (infrastructure/demo-domainandinfra/admin-domain) with a service binding to the Worker. Deploy the admin proxy withpnpm --filter @nextbase/admin cf:deploy:domainand point the admin DNS CNAME at itspages.devhost, or attach a custom domain directly to each Worker instead.
Smoke-check the live app
- Open
/sign-upon the web origin, create an account, and confirm the verification email arrives through Resend. - Sign in, complete
/onboarding, create a workspace and a project. - Grant your account the
adminrole in the production database (see authentication), open/loginon the admin origin and confirm you land on/app-admin. - Open
/mcp/connections/useron the web origin and/mcp/connections/adminon the admin origin, confirm the displayed addresses use the right origins, then connect an agent and callprojects_list. - From the admin users section, start an impersonation and confirm you land on the web app read-only and that Stop impersonating returns you to the admin origin.
- Request the jobs Worker's
/healthendpoint and check thatpendinganddeadcounts stay low. - Watch Workers Logs for
jobs.retrying,jobs.deadandjobs.dead_letteredevents after the first minute cron. - Send a signed Stripe test webhook and upload a public and a private media file if you use those features.
Rollback
Roll application code back by deploying the previous SHA with the same cf:deploy:* script, web first, then admin and jobs. Do not reverse an applied migration; use a forward repair migration or D1 Time Travel during an incident. The web Worker returns an X-Request-Id header on every response; start any investigation from that ID in operational_exceptions, as described in docs/operations.md.
If the Worker deploys but login or the database fails, see troubleshooting.