Environment Management
Patterns for managing multiple deployment environments.
Overview#
Proper environment management ensures safe deployments and configuration. This pattern covers:
- Environment variable structure
- Environment-specific configuration
- Feature flags per environment
- Secret management
- Preview environments
Prerequisites#
npm install zodCode Example#
Environment Variable Schema#
1// lib/env.ts
2import { z } from 'zod'
3
4const envSchema = z.object({
5 // Runtime environment
6 NODE_ENV: z.enum(['development', 'production', 'test']),
7 VERCEL_ENV: z.enum(['development', 'preview', 'production']).optional(),
8
9 // Database
10 DATABASE_URL: z.string().url(),
11
12 // Auth
13 NEXTAUTH_SECRET: z.string().min(32),
14 NEXTAUTH_URL: z.string().url(),
15
16 // API URLs
17 NEXT_PUBLIC_APP_URL: z.string().url(),
18 NEXT_PUBLIC_API_URL: z.string().url().optional(),
19
20 // Third-party services
21 STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
22 STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
23 RESEND_API_KEY: z.string().startsWith('re_'),
24
25 // Feature flags
26 ENABLE_ANALYTICS: z.coerce.boolean().default(false),
27 ENABLE_BETA_FEATURES: z.coerce.boolean().default(false)
28})
29
30function validateEnv() {
31 const parsed = envSchema.safeParse(process.env)
32
33 if (!parsed.success) {
34 console.error('Invalid environment variables:')
35 console.error(JSON.stringify(parsed.error.flatten().fieldErrors, null, 2))
36 throw new Error('Invalid environment configuration')
37 }
38
39 return parsed.data
40}
41
42export const env = validateEnv()
43export type Env = z.infer<typeof envSchema>Environment Files Structure#
1# .env.example - Template for developers
2DATABASE_URL="postgresql://localhost:5432/myapp"
3NEXTAUTH_SECRET="your-secret-here"
4NEXTAUTH_URL="http://localhost:3000"
5NEXT_PUBLIC_APP_URL="http://localhost:3000"
6
7# .env.local - Local development (git ignored)
8DATABASE_URL="postgresql://localhost:5432/myapp_dev"
9NEXTAUTH_SECRET="dev-secret-32-characters-long!!"
10NEXTAUTH_URL="http://localhost:3000"
11NEXT_PUBLIC_APP_URL="http://localhost:3000"
12ENABLE_BETA_FEATURES="true"
13
14# .env.test - Test environment
15DATABASE_URL="postgresql://localhost:5432/myapp_test"
16NEXTAUTH_SECRET="test-secret-32-characters-long!!"
17
18# .env.production - Production values (git ignored, in CI secrets)
19# Set via Vercel dashboard or CI/CD secretsEnvironment-Aware Configuration#
1// lib/config.ts
2import { env } from './env'
3
4interface Config {
5 isProduction: boolean
6 isDevelopment: boolean
7 isPreview: boolean
8 baseUrl: string
9 apiUrl: string
10 features: {
11 analytics: boolean
12 betaFeatures: boolean
13 debugMode: boolean
14 }
15 rateLimit: {
16 window: number
17 max: number
18 }
19}
20
21export function getConfig(): Config {
22 const isProduction = env.NODE_ENV === 'production' &&
23 env.VERCEL_ENV === 'production'
24 const isDevelopment = env.NODE_ENV === 'development'
25 const isPreview = env.VERCEL_ENV === 'preview'
26
27 return {
28 isProduction,
29 isDevelopment,
30 isPreview,
31 baseUrl: env.NEXT_PUBLIC_APP_URL,
32 apiUrl: env.NEXT_PUBLIC_API_URL ?? env.NEXT_PUBLIC_APP_URL,
33 features: {
34 analytics: isProduction && env.ENABLE_ANALYTICS,
35 betaFeatures: !isProduction || env.ENABLE_BETA_FEATURES,
36 debugMode: isDevelopment
37 },
38 rateLimit: {
39 window: isProduction ? 60 : 1,
40 max: isProduction ? 100 : 1000
41 }
42 }
43}
44
45export const config = getConfig()Dynamic Base URL#
1// lib/url.ts
2export function getBaseUrl(): string {
3 // Browser
4 if (typeof window !== 'undefined') {
5 return ''
6 }
7
8 // Vercel preview deployments
9 if (process.env.VERCEL_URL) {
10 return `https://${process.env.VERCEL_URL}`
11 }
12
13 // Production
14 if (process.env.NEXT_PUBLIC_APP_URL) {
15 return process.env.NEXT_PUBLIC_APP_URL
16 }
17
18 // Local development
19 return `http://localhost:${process.env.PORT ?? 3000}`
20}
21
22// Usage in tRPC, API routes, etc.
23const baseUrl = getBaseUrl()Feature Flags by Environment#
1// lib/features.ts
2import { env } from './env'
3
4interface FeatureFlags {
5 newDashboard: boolean
6 aiFeatures: boolean
7 betaCheckout: boolean
8 darkMode: boolean
9}
10
11const featuresByEnvironment: Record<string, Partial<FeatureFlags>> = {
12 development: {
13 newDashboard: true,
14 aiFeatures: true,
15 betaCheckout: true,
16 darkMode: true
17 },
18 preview: {
19 newDashboard: true,
20 aiFeatures: true,
21 betaCheckout: true,
22 darkMode: true
23 },
24 production: {
25 newDashboard: false, // Gradual rollout
26 aiFeatures: true,
27 betaCheckout: false,
28 darkMode: true
29 }
30}
31
32export function getFeatureFlags(): FeatureFlags {
33 const environment = env.VERCEL_ENV ?? env.NODE_ENV
34 const flags = featuresByEnvironment[environment] ?? {}
35
36 return {
37 newDashboard: false,
38 aiFeatures: false,
39 betaCheckout: false,
40 darkMode: true,
41 ...flags
42 }
43}
44
45export const features = getFeatureFlags()
46
47// Usage
48if (features.newDashboard) {
49 // Show new dashboard
50}Database URL by Environment#
1// lib/db.ts
2import { PrismaClient } from '@prisma/client'
3
4function getDatabaseUrl(): string {
5 const url = process.env.DATABASE_URL
6
7 if (!url) {
8 throw new Error('DATABASE_URL is required')
9 }
10
11 // Add connection pooling for production
12 if (process.env.NODE_ENV === 'production') {
13 const poolUrl = new URL(url)
14 poolUrl.searchParams.set('connection_limit', '10')
15 poolUrl.searchParams.set('pool_timeout', '10')
16 return poolUrl.toString()
17 }
18
19 return url
20}
21
22const globalForPrisma = globalThis as unknown as {
23 prisma: PrismaClient | undefined
24}
25
26export const prisma = globalForPrisma.prisma ?? new PrismaClient({
27 datasources: {
28 db: { url: getDatabaseUrl() }
29 },
30 log: process.env.NODE_ENV === 'development'
31 ? ['query', 'error', 'warn']
32 : ['error']
33})
34
35if (process.env.NODE_ENV !== 'production') {
36 globalForPrisma.prisma = prisma
37}Vercel Environment Configuration#
1// vercel.json
2{
3 "env": {
4 "NEXT_PUBLIC_APP_URL": "https://myapp.com"
5 },
6 "build": {
7 "env": {
8 "DATABASE_URL": "@database-url-production"
9 }
10 }
11}GitHub Environment Secrets#
1# .github/workflows/deploy.yml
2name: Deploy
3
4on:
5 push:
6 branches: [main, staging]
7
8jobs:
9 deploy:
10 runs-on: ubuntu-latest
11 environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
12 steps:
13 - uses: actions/checkout@v4
14
15 - name: Deploy
16 run: |
17 echo "Deploying to ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}"
18 env:
19 DATABASE_URL: ${{ secrets.DATABASE_URL }}
20 STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}Preview Environment Database#
1// lib/preview-db.ts
2export async function getPreviewDatabaseUrl(): Promise<string> {
3 const branchName = process.env.VERCEL_GIT_COMMIT_REF
4
5 if (!branchName || process.env.VERCEL_ENV !== 'preview') {
6 return process.env.DATABASE_URL!
7 }
8
9 // Use Neon or PlanetScale branch databases
10 const sanitizedBranch = branchName
11 .replace(/[^a-zA-Z0-9]/g, '-')
12 .toLowerCase()
13 .slice(0, 30)
14
15 // Create branch database if it doesn't exist
16 // This would call your database provider's API
17 const branchDbUrl = await createBranchDatabase(sanitizedBranch)
18
19 return branchDbUrl
20}Usage Instructions#
- Create
.env.examplewith all required variables - Use Zod to validate environment variables at startup
- Create environment-specific configurations
- Set up feature flags per environment
- Configure CI/CD with environment-specific secrets
Best Practices#
- Validate early - Check environment variables at startup
- Never commit secrets - Use
.env.localand CI secrets - Use typed config - Type-safe access to configuration
- Environment parity - Keep environments as similar as possible
- Preview databases - Use isolated databases for previews
- Feature flags - Enable gradual rollouts
- Document variables - Keep
.env.exampleup to date
Related Patterns#
- Secrets Management - Secure secret handling
- CI/CD - Deployment pipelines
- Docker - Container configuration
- Feature Flags - Feature management