Skip to main content

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:

  1. 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
  2. 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 development or production
  • npm install - NODE_ENV=production skips devDependencies
  • Third-party libraries - Most check NODE_ENV === 'production' for optimization
  • Framework assumptions - Tools expect NODE_ENV to be development, test, or production

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

VariablePurposeValid ValuesWho Sets It
NODE_ENVBuild/runtime modedevelopment, test, productionFramework/tooling
APP_ENVDeployment stage (server-only)development, staging, production, previewDeveloper/CI/CD
NEXT_PUBLIC_APP_ENVDeployment stage (client-exposed)Same as APP_ENVDeveloper/CI/CD

Rules

  1. NEVER manually set NODE_ENV in package.json scripts

    • "build": "next build" (Next.js sets NODE_ENV=production)
    • "build": "NODE_ENV=staging next build"
  2. 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
    }
  3. 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;
  4. Client-side stage detection

    // ✅ CORRECT - Use NEXT_PUBLIC_APP_ENV for client-side
    const isStaging = process.env.NEXT_PUBLIC_APP_ENV === 'staging';

Environment Matrix

EnvironmentNODE_ENVAPP_ENVUse Case
Local DevdevelopmentdevelopmentDeveloper machines, hot reload
Test SuitetestdevelopmentRunning vitest/jest tests
Staging BuildproductionstagingPre-prod testing with prod builds
Prod BuildproductionproductionLive customer environment
PR PreviewproductionpreviewTemporary 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 ConfigAPP_ENV ValueWhy Different?
devdevelopmentConfig name is short label, APP_ENV matches NODE_ENV style
stgstaging"staging" is more recognizable than "stg"
prdproduction"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_ENV is shorter and widely used in industry

Decision: APP_ENV is the industry standard and shorter to type.


Consequences

Positive

  1. Next.js Optimizations Work

    • Production builds always use NODE_ENV=production
    • Staging gets full optimization (minification, tree-shaking)
    • Preview deployments are production-quality
  2. Framework Compatibility

    • All libraries check NODE_ENV === 'production' correctly
    • npm/pnpm install works as expected
    • No framework hacks or workarounds needed
  3. Clear Separation

    • NODE_ENV → How to build
    • APP_ENV → Where it's deployed
    • No confusion between build mode and deployment stage
  4. 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';
  5. 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

  1. Two Variables to Manage

    • Must set both NODE_ENV and APP_ENV
    • Mitigation: Doppler manages both automatically
  2. Team Training

    • Developers must understand the distinction
    • Mitigation: Document clearly in CLAUDE.md and quickstart guides
  3. CI/CD Complexity

    • Must explicitly set NODE_ENV=production and APP_ENV=staging
    • Mitigation: Template workflows provided, copy-paste ready

Validation Checklist

Development

  • echo $NODE_ENVdevelopment
  • echo $APP_ENVdevelopment
  • Hot reload works (Next.js dev mode)
  • Source maps available

Test

  • NODE_ENV=test pnpm test uses DATABASE_TEST_URL
  • Tests run in isolation
  • No production optimizations

Staging Build

  • NODE_ENV=production pnpm build succeeds
  • APP_ENV=staging set in runtime
  • Build is optimized (minified, tree-shaken)
  • Debug features enabled (if configured)

Production Build

  • NODE_ENV=production pnpm build succeeds
  • APP_ENV=production set 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

References