ADR-0055: Environment Variable Strategy (NODE_ENV vs APP_ENV)
Status
Accepted - 2025-10-28
Context
The platform needs to distinguish between two different concepts:
- 
Build Mode - How the code is built/optimized (Node.js/Next.js convention) - development- Source maps, hot reload, no minification
- test- Testing mode with test database
- production- Optimized, minified, production builds
 
- 
Deployment Stage - Where the application is deployed - development- Local development environment
- staging- Pre-production testing environment
- production- Live production environment
- preview- Temporary preview deployments (PRs, feature branches)
 
The Problem
Many teams incorrectly use NODE_ENV=staging or NODE_ENV=preview, which breaks:
- Next.js optimizations - Next.js only recognizes developmentorproduction
- npm install - NODE_ENV=productionskips devDependencies
- Third-party libraries - Most check NODE_ENV === 'production'for optimization
- Framework assumptions - Tools expect NODE_ENV to be development,test, orproduction
Anti-pattern Example:
# ❌ WRONG - Breaks Next.js production optimizations in staging
NODE_ENV=staging pnpm build
Decision
Use NODE_ENV for build mode ONLY. Use APP_ENV for deployment stage.
Variable Definitions
| Variable | Purpose | Valid Values | Who Sets It | 
|---|---|---|---|
| NODE_ENV | Build/runtime mode | development,test,production | Framework/tooling | 
| APP_ENV | Deployment stage (server-only) | development,staging,production,preview | Developer/CI/CD | 
| NEXT_PUBLIC_APP_ENV | Deployment stage (client-exposed) | Same as APP_ENV | Developer/CI/CD | 
Rules
- 
NEVER manually set NODE_ENV in package.json scripts - ✅ "build": "next build"(Next.js sets NODE_ENV=production)
- ❌ "build": "NODE_ENV=staging next build"
 
- ✅ 
- 
Use APP_ENV for stage-specific behavior // ✅ CORRECT - Check deployment stage
 if (process.env.APP_ENV === 'staging') {
 // Enable staging-specific features
 }
 // ✅ CORRECT - Check build mode
 if (process.env.NODE_ENV === 'production') {
 // Enable production optimizations
 }
- 
Database selection // ✅ CORRECT - Use NODE_ENV for test database only
 const url = process.env.NODE_ENV === 'test'
 ? process.env.DATABASE_TEST_URL
 : process.env.DATABASE_URL;
- 
Client-side stage detection // ✅ CORRECT - Use NEXT_PUBLIC_APP_ENV for client-side
 const isStaging = process.env.NEXT_PUBLIC_APP_ENV === 'staging';
Environment Matrix
| Environment | NODE_ENV | APP_ENV | Use Case | 
|---|---|---|---|
| Local Dev | development | development | Developer machines, hot reload | 
| Test Suite | test | development | Running vitest/jest tests | 
| Staging Build | production | staging | Pre-prod testing with prod builds | 
| Prod Build | production | production | Live customer environment | 
| PR Preview | production | preview | Temporary preview deployments | 
Implementation
1. Environment Files
.env.example
# Build mode (set by framework, rarely override)
NODE_ENV=development
# Deployment stage (set manually or by CI/CD)
APP_ENV=development
# Database selection
DATABASE_ENVIRONMENT=local
DATABASE_URL=postgresql://tvl_user:tvl_password@postgres:5432/tvl_dev
DATABASE_TEST_URL=postgresql://tvl_user:tvl_password@postgres:5432/tvl_test
apps/web/.env.example
# Deployment stage (client-exposed)
NEXT_PUBLIC_APP_ENV=development
# API endpoints
NEXT_PUBLIC_API_URL=http://localhost:4000
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=xxx
2. Next.js Configuration
apps/web/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  transpilePackages: ['@tvl/database', '@tvl/types'],
  // Expose APP_ENV to client
  env: {
    NEXT_PUBLIC_APP_ENV: process.env.APP_ENV || process.env.NEXT_PUBLIC_APP_ENV || 'development',
  },
  experimental: {
    serverActions: {
      bodySizeLimit: '2mb',
    },
  },
}
module.exports = nextConfig
3. Doppler Configuration
IMPORTANT: Config Names vs Secret Values
Doppler config names (organizational labels) don't need to match APP_ENV values:
| Doppler Config | APP_ENV Value | Why Different? | 
|---|---|---|
| dev | development | Config name is short label, APP_ENV matches NODE_ENV style | 
| stg | staging | "staging" is more recognizable than "stg" | 
| prd | production | "production" is more recognizable than "prd" | 
This separation allows:
- ✅ Short, memorable config names for daily use (doppler run --config dev)
- ✅ Standard, framework-compatible values in the actual secrets
- ✅ Consistency with NODE_ENV naming convention
Automated Setup:
# Run the setup script to configure all environments automatically
./scripts/setup-doppler-env.sh
Or set manually for each environment:
Development (dev)
NODE_ENV=development
APP_ENV=development
DATABASE_ENVIRONMENT=local
Staging (stg)
NODE_ENV=production  # ← Production builds!
APP_ENV=staging      # ← But staging features
DATABASE_ENVIRONMENT=supabase-staging
Production (prd)
NODE_ENV=production
APP_ENV=production
DATABASE_ENVIRONMENT=supabase-production
4. CI/CD Workflows
.github/workflows/deploy-staging.yml
- name: Build application
  run: pnpm build
  env:
    NODE_ENV: production      # ← Production build
    APP_ENV: staging          # ← Staging deployment
    DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
Alternatives Considered
Alternative 1: Use NODE_ENV for everything
Rejected
Pros:
- Simpler (one variable)
- No additional configuration
Cons:
- Breaks Next.js optimizations when using NODE_ENV=staging
- Breaks npm/pnpm install (skips devDependencies)
- Incompatible with most libraries and frameworks
- Violates Node.js ecosystem conventions
Decision: This anti-pattern causes more problems than it solves.
Alternative 2: Use custom environment files (.env.staging)
Rejected
Pros:
- Clear separation of environments
- Popular in some frameworks (Laravel, Rails)
Cons:
- Doesn't solve the NODE_ENV confusion
- Still need APP_ENV for runtime behavior
- Adds complexity to Doppler setup
Decision: Environment files don't address the core NODE_ENV vs stage issue.
Alternative 3: Use DEPLOYMENT_ENV instead of APP_ENV
Rejected
Pros:
- More explicit naming
Cons:
- Longer variable name
- Less common convention
- APP_ENVis shorter and widely used in industry
Decision: APP_ENV is the industry standard and shorter to type.
Consequences
Positive
- 
Next.js Optimizations Work - Production builds always use NODE_ENV=production
- Staging gets full optimization (minification, tree-shaking)
- Preview deployments are production-quality
 
- Production builds always use 
- 
Framework Compatibility - All libraries check NODE_ENV === 'production'correctly
- npm/pnpm install works as expected
- No framework hacks or workarounds needed
 
- All libraries check 
- 
Clear Separation - NODE_ENV→ How to build
- APP_ENV→ Where it's deployed
- No confusion between build mode and deployment stage
 
- 
Feature Flags // Enable debug panel in staging only
 const showDebugPanel = process.env.APP_ENV === 'staging';
 // Enable analytics in production only
 const enableAnalytics = process.env.APP_ENV === 'production';
- 
API Behavior // Strict validation in production, lenient in staging
 const strictMode = process.env.APP_ENV === 'production';
 // Detailed errors in dev/staging, generic in production
 const verboseErrors = ['development', 'staging'].includes(process.env.APP_ENV);
Negative
- 
Two Variables to Manage - Must set both NODE_ENVandAPP_ENV
- Mitigation: Doppler manages both automatically
 
- Must set both 
- 
Team Training - Developers must understand the distinction
- Mitigation: Document clearly in CLAUDE.md and quickstart guides
 
- 
CI/CD Complexity - Must explicitly set NODE_ENV=productionandAPP_ENV=staging
- Mitigation: Template workflows provided, copy-paste ready
 
- Must explicitly set 
Validation Checklist
Development
-  echo $NODE_ENV→development
-  echo $APP_ENV→development
- Hot reload works (Next.js dev mode)
- Source maps available
Test
-  NODE_ENV=test pnpm testusesDATABASE_TEST_URL
- Tests run in isolation
- No production optimizations
Staging Build
-  NODE_ENV=production pnpm buildsucceeds
-  APP_ENV=stagingset in runtime
- Build is optimized (minified, tree-shaken)
- Debug features enabled (if configured)
Production Build
-  NODE_ENV=production pnpm buildsucceeds
-  APP_ENV=productionset in runtime
- No debug features
- Analytics/monitoring enabled
Migration Guide
Step 1: Update .env files
# Add APP_ENV to all .env files
echo "APP_ENV=development" >> .env.local
Step 2: Update Doppler
# For each environment (dev, stg, prd)
doppler secrets set APP_ENV=development --config dev
doppler secrets set APP_ENV=staging --config stg
doppler secrets set APP_ENV=production --config prd
Step 3: Update code
// Before (WRONG)
if (process.env.NODE_ENV === 'staging') { ... }
// After (CORRECT)
if (process.env.APP_ENV === 'staging') { ... }
Step 4: Update CI/CD
# Before (WRONG)
env:
  NODE_ENV: staging
# After (CORRECT)
env:
  NODE_ENV: production  # Build mode
  APP_ENV: staging      # Deployment stage