BetterStarter logoBetterStarter
Features

Environment Variables

Type-safe environment variable validation with Zod and TypeScript.

Overview

BetterStarter uses Zod for runtime validation and TypeScript for compile-time type safety. Environment variables are validated on startup.

Configuration File

Environment variables are defined in src/env.ts:

import { z } from 'zod'

const envSchema = z.object({
  // Required
  APP_BASE_URL: z.string().url(),
  DATABASE_URL: z.string(),
  
  // Optional with defaults
  NODE_ENV: z.enum(['development', 'production']).default('development'),
  
  // Features
  STRIPE_SECRET_KEY: z.string().optional(),
  GOOGLE_OAUTH_CLIENT_ID: z.string().optional(),
})

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

Usage

Import and use environment variables:

import { env } from '@/env'

// Strongly typed - TypeScript knows these exist
console.log(env.DATABASE_URL)
console.log(env.STRIPE_SECRET_KEY) // May be undefined if optional

Local Development

Create .env.local file (git-ignored):

# Copy from sample
cp .env.sample .env.local

# Edit with your values
APP_BASE_URL=http://localhost:3000
DATABASE_URL=postgres://user:password@localhost:5432/betterstarter
BETTER_AUTH_SECRET=<run: openssl rand -base64 32>
STRIPE_SECRET_KEY=sk_test_...

Never commit .env.local to version control.

Production Deployment

Set environment variables in your hosting platform:

Vercel

vercel env add APP_BASE_URL
vercel env add DATABASE_URL
vercel env add STRIPE_SECRET_KEY
# ... etc

Netlify

netlify env:set APP_BASE_URL "https://mydomain.com"
netlify env:set DATABASE_URL "postgres://..."

Docker / Self-Hosted

APP_BASE_URL=https://mydomain.com \
DATABASE_URL=postgres://... \
pnpm build && pnpm start

Schema Validation

Zod provides runtime validation:

const schema = z.object({
  PORT: z.coerce.number().positive().default(3000),
  DEBUG: z.string().transform(v => v === 'true').default('false'),
  API_KEY: z.string().min(32, 'API key must be at least 32 chars'),
})

Validation Types

TypeExample
z.string()"hello"
z.string().url()"https://example.com"
z.coerce.number()"3000" → 3000
z.enum(['a', 'b'])"a" or "b" only
.default(value)Falls back if missing
.optional()Can be undefined

Server-Only Variables

Variables only needed on the server should be validated on startup:

// src/env.ts - Validates at startup
export const env = envSchema.parse(process.env)

// Server functions can safely access
import { env } from '@/env'

export const getSecretData = createServerFn().handler(async () => {
  const apiKey = env.STRIPE_SECRET_KEY // Always defined
  // ...
})

Startup Errors

Missing or invalid variables cause startup failure:

$ pnpm dev
Error: (env) "DATABASE_URL" is required

This ensures your app won't start with incomplete configuration.

Example Schema

import { z } from 'zod'

export const env = z.object({
  // App
  NODE_ENV: z.enum(['development', 'production']).default('development'),
  APP_BASE_URL: z.string().url(),
  
  // Database
  DATABASE_URL: z.string(),
  
  // Auth
  BETTER_AUTH_URL: z.string().url(),
  BETTER_AUTH_SECRET: z.string().min(32),
  
  // Optional services
  STRIPE_SECRET_KEY: z.string().optional(),
  GOOGLE_OAUTH_CLIENT_ID: z.string().optional(),
  PLUNK_API_KEY: z.string().optional(),
}).parse(process.env)

export type Env = typeof env

Secrets Management

Never commit secrets like API keys. Some platforms automatically mask secrets in logs:

  • Vercel: Uses built-in secrets manager
  • AWS: Use Secrets Manager or Parameter Store
  • Self-hosted: Use environment files or GitOps (ArgoCD, Flux)

On this page