Environment variables
Every variable in the TanStack + Neon starter's env files, what it does and where the value comes from.
The kit ships two committed templates: .env.example at the repository root for the web app, the worker, the database scripts and Docker Compose, and apps/admin/.env.example for the admin app. Your .env files are ignored by git.
Validation lives in apps/web/src/server/env.ts (zod), which both the web and the admin server import. Every variable there has a local default that matches .env.example, so a missing value falls back to the Docker setup rather than failing. In production set each one explicitly. The database package has its own one-field schema in apps/database/src/env.ts that reads only DATABASE_DIRECT_URL.
All variables are server-only. The React Router app is server-rendered by Express, nothing uses a VITE_ prefix, and no value is handed to the browser.
Nothing loads .env into Node for you
Docker Compose reads .env for the port variables. The dev, worker, migrate and seed scripts run plain tsx and read process.env only. Export the variables you change in the shell that runs the scripts, or run them through an env loader of your choice. On Vercel, set them in the project's environment settings.
Create the file only when it is missing:
[ -f .env ] || cp .env.example .envApplication
| Variable | Required | What it is for | Where to get the value |
|---|---|---|---|
NODE_ENV | Yes | development, test or production. Controls the CSP, secure cookies, Vite middleware and static serving | development locally, production when deployed |
PORT | Yes | Port the Express server listens on. Default 3000 | Your choice. Match APP_BASE_URL. The admin dev script sets 3001 |
APP_BASE_URL | Yes | Canonical origin of the app that reads it. Used by Better Auth as baseURL, as the origin check for impersonation requests, and as the MCP issuer and resource origin | http://localhost:3000 for web, http://localhost:3001 for admin, your HTTPS domains in production |
TRUSTED_ORIGINS | Yes | Comma-separated browser origins allowed to make unsafe /api/v1 requests and Better Auth calls | Usually the same as APP_BASE_URL. Add preview origins if you use them |
BETTER_AUTH_SECRET | Yes | Signs sessions and cookies. Must be at least 32 characters | Generate at least 32 random bytes. Never reuse the example value |
WEB_APP_URL | Yes | The product origin. The admin app builds the impersonation handoff link (/auth/impersonation) on it | http://localhost:3000 locally, the web deployment's origin in production |
ADMIN_APP_URL | Yes | The admin origin. The web app redirects to {ADMIN_APP_URL}/app-admin/users when a support session ends | http://localhost:3001 locally, the admin deployment's origin in production |
Database
The kit uses four PostgreSQL credentials. Keep them separate; the whole RLS model depends on it. See authentication for what each role may do.
| Variable | Required | What it is for | Where to get the value |
|---|---|---|---|
DATABASE_DIRECT_URL | Yes | Migrations, db:seed, db:check, admin:grant and drizzle-kit. Connects as migration_owner on the direct (non-pooled) endpoint | Locally postgresql://migration_owner:...@localhost:35432/nextbase. In Neon, the direct endpoint with the migration_owner password |
DATABASE_URL | Yes | Tenant-aware application queries and outbox inserts as app_runtime through PgBouncer transaction pooling | Locally port 36432. In Neon, the pooled endpoint with the app_runtime password |
AUTH_DATABASE_URL | Yes | Better Auth tables, support handoff records and MCP credential storage as auth_runtime | Pooled endpoint, auth_runtime password |
WORKER_DATABASE_URL | Yes | Outbox job claims and completions as worker_runtime | Pooled endpoint, worker_runtime password |
POSTGRES_PORT | Local only | Host port Compose publishes for PostgreSQL. Default 35432 | Change when the port is taken, then update DATABASE_DIRECT_URL |
PGBOUNCER_PORT | Local only | Host port Compose publishes for PgBouncer. Default 36432 | Change when the port is taken, then update the three runtime URLs |
Production connection strings must end with ?sslmode=verify-full. Both apps use the same four URLs.
| Variable | Required | What it is for | Where to get the value |
|---|---|---|---|
SMTP_HOST | Yes | SMTP server used by the outbox worker (nodemailer). When it is smtp.resend.com, the kit adds a Resend-Idempotency-Key header per delivery | localhost for Mailpit. Your provider's SMTP host in production |
SMTP_PORT | Yes | SMTP port. Default 31025 for Mailpit | Provider docs. 587 is the usual production value |
SMTP_USER | Production | SMTP username. Authentication is enabled only when both user and password are set | Your provider. Leave empty for Mailpit |
SMTP_PASSWORD | Production | SMTP password or API key | Your provider. Leave empty for Mailpit |
SMTP_SECURE | Optional | true forces SMTPS. When omitted, the kit infers it from the port (465 is secure) | Only set it if your provider needs an override |
MAILPIT_UI_PORT | Local only | Host port for the Mailpit web inbox. Default 38025 | Change when the port is taken |
EMAIL_FROM | Yes | Sender shown on every email | A verified sender on your domain, for example Nextbase <no-reply@example.com> |
Delivery is SMTP only; there is no RESEND_API_KEY in this kit. To use Resend, put its SMTP host in SMTP_HOST and the API key in SMTP_PASSWORD. See emails with Resend for the provider side.
Object storage
These variables configure the local MinIO container and are listed in .env.example for the storage features you add. Nothing in apps/web or apps/admin reads them yet; media_assets exists in the schema without an upload flow.
| Variable | Required | What it is for | Where to get the value |
|---|---|---|---|
MINIO_API_PORT | Local only | Host port for the MinIO API. Default 39000 | Change together with S3_ENDPOINT |
MINIO_CONSOLE_PORT | Local only | Host port for the MinIO console. Default 39001 | Change when the port is taken |
S3_ENDPOINT | Not read yet | S3-compatible endpoint. Default http://localhost:39000 | Your bucket's endpoint when you wire uploads |
S3_REGION | Not read yet | Bucket region. Default us-east-1 | Your provider |
S3_BUCKET | Not read yet | Bucket name. Default nextbase. Compose does not create the bucket; make it in the MinIO console | Your production bucket |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | Not read yet | Credentials. The defaults match the MinIO root user in docker-compose.yml | Least-privilege credentials in production |
Social login
All six are optional. A provider is registered with Better Auth only when both of its values are set. The shipped login page has no provider buttons; use authClient.signIn.social from apps/web/app/lib/auth-client.ts when you add them.
| Variable | Required | What it is for | Where to get the value |
|---|---|---|---|
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | Optional | Google sign-in | Google Cloud console OAuth client with your APP_BASE_URL callback |
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET | Optional | GitHub sign-in | GitHub OAuth app |
TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET | Optional | X sign-in | X developer portal |
Placeholders for later features
These four are in .env.example so the file has a stable shape, but nothing reads them in this kit. /api/v1/webhooks/stripe exists and answers 501 STRIPE_NOT_CONFIGURED until you implement billing.
| Variable | Status |
|---|---|
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET | Not read. Billing tables exist, no Stripe code |
SENTRY_DSN | Not read. No error monitoring client is installed |
POSTHOG_KEY | Not read. No analytics client is installed |
Admin app template
apps/admin/.env.example contains only the five values that differ from the web app:
| Variable | Value in the template | Notes |
|---|---|---|
APP_BASE_URL | http://localhost:3001 | The admin app's own origin |
WEB_APP_URL | http://localhost:3000 | Where impersonation handoffs are sent |
ADMIN_APP_URL | http://localhost:3001 | Same as APP_BASE_URL for the admin app |
TRUSTED_ORIGINS | http://localhost:3001 | The admin origin only |
PORT | 3001 | Matches the dev script |
The template's comment says the rest: use the same AUTH_DATABASE_URL, DATABASE_URL, WORKER_DATABASE_URL and BETTER_AUTH_SECRET as the web app. Cookies are host-only, so the two origins never share a session even with a shared secret; docs/admin-app.md describes giving each deployment its own secret if you prefer.
Platform variables not in the example files
| Variable | Where | What it is for |
|---|---|---|
CRON_SECRET | Vercel project for apps/web | Bearer token Vercel Cron sends to /internal/jobs/run. The kit requires at least 32 characters and rejects the request otherwise |
VERCEL | Set by Vercel | When present, both servers export the Express app instead of listening, and emails are dispatched with waitUntil plus the cron |
NEXTBASE_WORKER_MODE | Set by the scripts | daemon makes worker.ts poll forever. The worker and worker:start scripts set it for you |
MCP_TEST_PG_URL | Local shell | Loopback pooled connection for apps/web/src/mcp/pg-store.integration.test.ts. The test skips without it and refuses non-loopback hosts |
Installation
Run the TanStack + Neon starter locally with Docker, apply migrations and seed data, start the web and admin apps, and create your first account.
Authentication
How Better Auth, global and workspace roles, the separate admin app and PostgreSQL row-level security work in the TanStack + Neon starter.