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 optionalLocal 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
# ... etcNetlify
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 startSchema 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
| Type | Example |
|---|---|
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 requiredThis 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 envSecrets 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)