Environment variables
Every variable in the Next.js + Cloudflare D1 starter's example files, which Worker reads it, and where the value comes from.
This kit splits configuration in three places:
apps/web/.dev.vars,apps/admin/.dev.varsandapps/platform-worker/.dev.varshold Worker secrets for local development. In production the same names become Wrangler secrets. All three files are gitignored.- The
varsblock in eachwrangler.jsoncholds non-secret defaults that are safe to commit. apps/web/.env.localholds values the Next.js toolchain reads outside the Worker, such asNEXT_PUBLIC_*build variables and the Sentry upload settings. It repeats some of the Worker values so plainnext devtooling has them too.setup.shcreates it from the example when it is missing.
Never put a real key in a vars block or in the example files. Run pnpm cf:typegen after adding or renaming a secret in a .dev.vars.example file; CI checks the generated worker-configuration.d.ts with pnpm cf:typegen:check.
apps/web/.dev.vars
Secrets for the web Worker. Copy from apps/web/.dev.vars.example.
| Variable | Required | Purpose | Where to get it |
|---|---|---|---|
BETTER_AUTH_SECRET | Required | Signs Better Auth sessions and tokens | Generate a random string of at least 32 characters |
STRIPE_SECRET_KEY | Required for billing | Creates checkout and portal sessions | Stripe dashboard, API keys |
STRIPE_WEBHOOK_SECRET | Required for billing | Verifies incoming webhook signatures | stripe listen locally, or the webhook endpoint in Stripe |
REALTIME_TOKEN_SECRET | Required | Signs channel-scoped realtime tokens; must match the admin and platform Workers exactly | Generate at least 32 random characters |
RESEND_API_KEY | Optional | Sends email when USE_LOCAL_EMAIL is not true | Resend dashboard |
OPENAI_API_KEY | Optional | Image generation and AI helpers | OpenAI dashboard |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | Optional | Enables Google sign-in when both are set | Google Cloud OAuth client |
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET | Optional | Enables GitHub sign-in when both are set | GitHub OAuth app |
TWITTER_API_KEY, TWITTER_API_SECRET | Optional | Enables X sign-in when both are set | X developer portal |
REVALIDATE_SECRET | Required | Protects the /api/revalidate route | Generate a random string |
All of these are server-only. Values shorter than 32 characters for REALTIME_TOKEN_SECRET are rejected at runtime.
apps/admin/.dev.vars
Secrets and local overrides for the admin Worker. Copy from apps/admin/.dev.vars.example. The first block repeats the web Worker's secret names; the admin app runs the same Better Auth, Stripe, email and provider code against the same D1 database.
| Variable | Required | Purpose | Where to get it |
|---|---|---|---|
BETTER_AUTH_SECRET | Required | Signs the admin app's own sessions; use a value independent from the web Worker | Generate a random string of at least 32 characters |
STRIPE_SECRET_KEY | Required for billing | Reads billing data for the payment gateway pages | Same key as the web Worker |
STRIPE_WEBHOOK_SECRET | Required for billing | Present for the shared Stripe module | Same value as the web Worker |
REALTIME_TOKEN_SECRET | Required | Same value as the web and platform Workers | Copy from apps/web/.dev.vars |
RESEND_API_KEY | Optional | Sends email when USE_LOCAL_EMAIL is not true | Resend dashboard |
OPENAI_API_KEY | Optional | AI helpers in the admin content tools | OpenAI dashboard |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | Optional | Google sign-in on the admin login page | Google Cloud OAuth client |
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET | Optional | GitHub sign-in on the admin login page | GitHub OAuth app |
TWITTER_API_KEY, TWITTER_API_SECRET | Optional | X sign-in on the admin login page | X developer portal |
REVALIDATE_SECRET | Required | Shared revalidation secret | Same value as the web Worker |
ADMIN_APP | Required | true marks this Worker as the admin app: admin cookie prefix, admin MCP audience, admin-only data paths | Keep true |
NEXT_PUBLIC_SITE_URL | Required | The admin app's own origin | http://localhost:3001 locally |
BETTER_AUTH_URL | Required | Better Auth base URL for the admin app | http://localhost:3001 locally |
NEXT_PUBLIC_WEB_APP_URL | Required | The customer web app's origin, used for the impersonation callback and media URLs | http://localhost:3000 locally |
The last four are also set in the vars block of apps/admin/wrangler.jsonc; the .dev.vars copies exist so next dev has them before the Wrangler bindings load.
apps/platform-worker/.dev.vars
Secrets for the background Worker. Copy from apps/platform-worker/.dev.vars.example.
| Variable | Required | Purpose | Where to get it |
|---|---|---|---|
REALTIME_TOKEN_SECRET | Required | Same value as the web and admin Workers | Copy from apps/web/.dev.vars |
INTERNAL_SERVICE_SECRET | Required | Protects the internal and operator recovery routes; missing, short or wrong values get a 404 | Generate an independent string of at least 32 characters |
RESEND_API_KEY | Optional | Delivers outbox emails in production | Resend dashboard |
STRIPE_SECRET_KEY | Required for billing | Reconciles invoices and charges during webhook projection, and runs product synchronization queued by the admin app | Same key as the web Worker |
Non-secret vars in wrangler.jsonc
These are committed. Edit them in the vars block, and in the env.production.vars block for your deployed Workers.
| Variable | Worker | Local default | Purpose |
|---|---|---|---|
BETTER_AUTH_URL | web, admin | http://localhost:3000 (web), http://localhost:3001 (admin) | Better Auth base URL; set to each Worker's public origin in production |
NEXT_PUBLIC_SITE_URL | web, admin | http://localhost:3000 (web), http://localhost:3001 (admin) | Each Worker's own origin, trusted origin for auth and the MCP issuer; public |
NEXT_PUBLIC_ADMIN_APP_URL | web | http://localhost:3001 | Where the impersonation banner sends admins back to (/app-admin/users on the admin origin) |
NEXT_PUBLIC_WEB_APP_URL | admin | http://localhost:3000 | The customer app's origin; impersonation redeems its token at <web origin>/auth/impersonate |
ADMIN_APP | admin | "true" | Switches the shared code into admin mode |
ADMIN_EMAIL | web, admin, platform | admin@example.com | The first account registered with this email gets the admin role; also the default sender address |
USE_LOCAL_EMAIL | web, admin, platform | true | When true, auth emails are also posted to Mailpit; set to false in production |
MAILPIT_API_URL | web, admin, platform | http://localhost:8025 | Mailpit API used for local delivery; empty in production |
INTERNAL_SERVICE_PRINCIPAL | platform | nextbase-operator-api | Non-secret identity recorded for operator actions |
MAIL_FROM | platform | not set locally | Optional verified sender for production emails; falls back to the payload sender or ADMIN_EMAIL |
NEXT_PUBLIC_SITE_URL is also needed at build time because it ends up in the browser bundle. The web Worker's cf:build:production script refuses to run until it is set in the shell; the admin Worker's cf:build:production script sets its own NEXT_PUBLIC_SITE_URL and NEXT_PUBLIC_WEB_APP_URL inline in apps/admin/package.json. See deployment.
apps/web/.env.local
Copy from apps/web/.env.local.example. It repeats the Worker values above and adds the ones below. Variables starting with NEXT_PUBLIC_ are exposed to the browser.
| Variable | Required | Public | Purpose | Where to get it |
|---|---|---|---|---|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | Required for billing | Yes | Stripe.js on the client | Stripe dashboard |
NEXT_PUBLIC_POSTHOG_API_KEY | Optional | Yes | PostHog analytics | PostHog project settings |
NEXT_PUBLIC_POSTHOG_HOST | Optional | Yes | PostHog ingestion host | PostHog project settings |
NEXT_PUBLIC_GA_ID | Optional | Yes | Google Analytics measurement ID | Google Analytics |
NEXT_PUBLIC_SENTRY_DSN | Optional | Yes | Sentry error reporting | Sentry project settings |
SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN | Optional | No | Source map upload during next build | Sentry organization settings |
LOG_LEVEL | Optional | No | Pino log level, default info | Your choice |
The repeated entries (NEXT_PUBLIC_SITE_URL, BETTER_AUTH_URL, BETTER_AUTH_SECRET, ADMIN_EMAIL, REALTIME_TOKEN_SECRET, the Stripe keys, USE_LOCAL_EMAIL, MAILPIT_API_URL, RESEND_API_KEY, OPENAI_API_KEY, the social provider keys and REVALIDATE_SECRET) should match what you put in .dev.vars and wrangler.jsonc. At runtime inside the Worker, the binding values win. The admin app has no .env.local.example; its Sentry build settings come from the shell when you build it.
Shell variables for scripts
| Variable | Used by | Default |
|---|---|---|
MAILPIT_SMTP_PORT, MAILPIT_HTTP_PORT | docker-compose.yml | 1025, 8025 |
PLATFORM_WORKER_PORT | scripts/dev-platform-worker.sh, scripts/preview-cloudflare.sh | 8788 |
PLATFORM_PERSIST_TO | scripts/dev-platform-worker.sh | apps/web/.wrangler/state |
NEXT_PUBLIC_SITE_URL | pnpm --filter web cf:build:production and cf:deploy:production | none; the build fails fast when it is missing |
D1_HTTP_URL | apps/database/drizzle.config.ts | the local SQLite file under .wrangler/state/v3/d1 |
Bindings, not variables
D1, R2, queues, Durable Objects, service bindings and rate limiters are declared in the three wrangler.jsonc files and arrive in env next to the variables above. The web and admin Workers each expect APP_DB, NEXT_TAG_CACHE_D1, MEDIA_BUCKET, NEXT_INC_CACHE_R2_BUCKET, PLATFORM, WORKER_SELF_REFERENCE, NEXT_CACHE_DO_QUEUE, AUTH_RATE_LIMITER and API_RATE_LIMITER. The platform Worker expects APP_DB, MEDIA_BUCKET, JOBS_QUEUE and REALTIME. Keep those names; change only the resource names and IDs behind them.
Installation
Clone the Next.js + Cloudflare D1 starter, create the local D1 databases and run the web app, the admin app and the platform Worker on your machine.
Authentication
How Better Auth is configured in the Next.js + Cloudflare D1 starter, how admins are assigned, where the admin app lives, and what protects application data.