Next Starter Logo
Security

Environment Variables

How Next Starter validates environment variables with Zod at startup, plus a complete reference for every required and optional variable: DATABASE_URL, BETTER_AUTH_SECRET, Stripe, SMTP2Go, Cloudflare R2, and more.

Overview

All environment variables are validated at application startup using Zod. If a required variable is missing or has an invalid value, the process throws immediately, before any request is served. Misconfigured deployments fail fast rather than failing silently at runtime.

The validation schema lives in lib/validations/env.ts and exports a typed env object used throughout the codebase.

How Validation Works

lib/validations/env.ts
import { z } from "zod";

const envSchema = z.object({
  DATABASE_URL: z.url(),
  DIRECT_DATABASE_URL: z.url().optional(),
  BETTER_AUTH_URL: z.url(),
  BETTER_AUTH_SECRET: z.string().min(32),
  NODE_ENV: z
    .enum(["development", "production", "test"])
    .default("development"),

  SMTP2GO_API_KEY: z.string().min(1),
  SENDER_EMAIL: z.email(),
  PUBLIC_URL: z.url(),

  STORAGE_S3_KEY: z.string().min(1),
  STORAGE_S3_SECRET: z.string().min(1),
  STORAGE_S3_REGION: z.string().min(1),
  STORAGE_S3_ENDPOINT: z.url(),
  STORAGE_S3_BUCKET: z.string().min(1),
  NEXT_PUBLIC_STORAGE_S3_CDN_URL: z.url(),
  NEXT_PUBLIC_APP_NAME: z.string().default("next-starter"),

  STRIPE_SECRET_KEY: z.string().min(1),
  STRIPE_PUBLISHABLE_KEY: z.string().min(1),
  STRIPE_WEBHOOK_SECRET: z.string().min(1),

  RECAPTCHA_SECRET_KEY: z.string().optional(),
  NEXT_PUBLIC_RECAPTCHA_SITE_KEY: z.string().optional(),

  GOOGLE_CLIENT_ID: z.string().min(1),
  GOOGLE_CLIENT_SECRET: z.string().min(1),

  NEXT_PUBLIC_BETTER_AUTH_URL: z.url().optional(),
});

export const env = envSchema.parse(process.env);

The env object is fully typed. TypeScript infers the shape from the schema, so you get autocompletion and type checking when you access env.DATABASE_URL, env.STRIPE_SECRET_KEY, etc.

Complete Variable Reference

Database

VariableRequiredDescription
DATABASE_URLYesPostgreSQL connection string used by Prisma for queries
DIRECT_DATABASE_URLNoDirect (non-pooled) connection string, required when using PgBouncer or a connection pooler
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
DIRECT_DATABASE_URL="postgresql://user:password@localhost:5432/mydb"

Authentication

VariableRequiredDescription
BETTER_AUTH_URLYesThe full URL of your application, used by Better Auth for redirects
BETTER_AUTH_SECRETYesSecret key for signing sessions (minimum 32 characters)
NEXT_PUBLIC_BETTER_AUTH_URLNoPublic-facing Better Auth URL, used by the client SDK
BETTER_AUTH_URL="https://yourdomain.com"
BETTER_AUTH_SECRET="a-very-long-random-secret-at-least-32-chars"

Generate a secure secret with:

pnpm dlx @better-auth/cli secret

Or using OpenSSL:

openssl rand -base64 32

Google OAuth

VariableRequiredDescription
GOOGLE_CLIENT_IDYesOAuth 2.0 client ID from Google Cloud Console
GOOGLE_CLIENT_SECRETYesOAuth 2.0 client secret from Google Cloud Console
GOOGLE_CLIENT_ID="123456789-abc.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="GOCSPX-..."

Email

VariableRequiredDescription
SMTP2GO_API_KEYYesAPI key from your SMTP2Go account
SENDER_EMAILYesFrom address for outgoing emails (must be verified in SMTP2Go)
PUBLIC_URLYesCanonical public base URL of the application
SMTP2GO_API_KEY="api-..."
SENDER_EMAIL="noreply@yourdomain.com"
PUBLIC_URL="https://yourdomain.com"

File Storage (Cloudflare R2)

VariableRequiredDescription
STORAGE_S3_KEYYesR2 access key ID
STORAGE_S3_SECRETYesR2 secret access key
STORAGE_S3_REGIONYesRegion identifier (use auto for R2)
STORAGE_S3_ENDPOINTYesR2 S3-compatible endpoint URL
STORAGE_S3_BUCKETYesName of the R2 bucket
NEXT_PUBLIC_STORAGE_S3_CDN_URLYesPublic CDN URL for serving stored files
STORAGE_S3_KEY="your-r2-access-key-id"
STORAGE_S3_SECRET="your-r2-secret-access-key"
STORAGE_S3_REGION="auto"
STORAGE_S3_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com"
STORAGE_S3_BUCKET="my-bucket"
NEXT_PUBLIC_STORAGE_S3_CDN_URL="https://cdn.yourdomain.com"

Stripe

VariableRequiredDescription
STRIPE_SECRET_KEYYesSecret key from your Stripe dashboard
STRIPE_PUBLISHABLE_KEYYesPublishable key from your Stripe dashboard
STRIPE_WEBHOOK_SECRETYesWebhook signing secret from a Stripe webhook endpoint
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_PUBLISHABLE_KEY="pk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..."

Use sk_test_ and pk_test_ keys during development. Get the webhook secret by running the Stripe CLI listener locally:

stripe listen --forward-to localhost:3000/api/auth/stripe/webhook

reCAPTCHA

VariableRequiredDescription
NEXT_PUBLIC_RECAPTCHA_SITE_KEYNoPublic site key for the reCAPTCHA v3 widget
RECAPTCHA_SECRET_KEYNoSecret key for server-side token verification

Both variables are optional. When omitted in development, reCAPTCHA verification passes automatically. See the reCAPTCHA guide for details.

Application

VariableRequiredDescription
NEXT_PUBLIC_APP_NAMENoDisplay name for the application (defaults to next-starter)
NODE_ENVNoRuntime environment: development, production, or test (defaults to development)
NEXT_TELEMETRY_DISABLEDNoSet to 1 to disable Next.js anonymous telemetry data collection

.env.example

Copy this as your starting point:

# Core
NODE_ENV="development"
PUBLIC_URL="http://localhost:3000"
NEXT_PUBLIC_APP_NAME="next-starter"

# Authentication
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000"
BETTER_AUTH_SECRET=""                    # Generate: pnpm dlx @better-auth/cli secret

# Google OAuth
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""

# Database
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/next_starter"
# DIRECT_DATABASE_URL=""                 # Direct connection for migrations

# File Storage (Cloudflare R2)
STORAGE_S3_KEY=""
STORAGE_S3_SECRET=""
STORAGE_S3_REGION="auto"
STORAGE_S3_ENDPOINT=""
STORAGE_S3_BUCKET="next-starter"
NEXT_PUBLIC_STORAGE_S3_CDN_URL=""

# Email
SMTP2GO_API_KEY=""
SENDER_EMAIL="noreply@yourdomain.com"

# reCAPTCHA (optional)
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=""
RECAPTCHA_SECRET_KEY=""

# Stripe
STRIPE_SECRET_KEY=""                     # Use sk_test_ keys for development
STRIPE_PUBLISHABLE_KEY=""                # Use pk_test_ keys for development
STRIPE_WEBHOOK_SECRET=""                 # From: stripe listen --forward-to localhost:3000/api/auth/stripe/webhook

# Misc
NEXT_TELEMETRY_DISABLED=1

Adding a New Environment Variable

  1. Add the variable to the schema in lib/validations/env.ts:
const envSchema = z.object({
  // existing variables...
  MY_API_KEY: z.string().min(1),
  MY_OPTIONAL_FLAG: z.string().optional(),
});
  1. Add the variable to your .env file and to any deployment environment (Railway, Fly.io, Docker, etc.).

  2. Access it anywhere in server-side code via the typed env object:

import { env } from "@/lib/validations/env";

const client = createClient({ apiKey: env.MY_API_KEY });

For NEXT_PUBLIC_ variables used in client components, access them directly via process.env.NEXT_PUBLIC_MY_VAR. Next.js inlines these at build time, so no import is needed. Adding them to the schema still validates they are present at startup.

Common Errors

ZodError: invalid_type on startup. A required variable is missing from your .env file or deployment environment. The error message includes the variable name.

BETTER_AUTH_SECRET too short. The secret must be at least 32 characters. Generate one with pnpm dlx @better-auth/cli secret or openssl rand -base64 32.

DATABASE_URL validation failure. Zod uses z.url(), which requires a valid URL format including the scheme. Ensure the value starts with postgresql:// or postgres://.

On this page