How We Manage Environments

Environment configuration, secrets management, and deployment strategies

How We Manage Environments

Your application runs in different contexts: your laptop, a staging server, production. Each has different needs. This guide covers how to manage these differences without drowning in configuration complexity.

The Core Principle

"Same code, different configuration."

Your application code shouldn't change between environments. Only configuration changes: database connections, API keys, feature flags, URLs.

Environment Types

┌─────────────────────────────────────────────────────────────┐
│                     DEVELOPMENT                              │
│  Local machine, hot reload, debug tools                     │
│  Database: Local / Docker                                   │
│  API Keys: Test/sandbox credentials                         │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                      STAGING                                 │
│  Production-like, for testing                               │
│  Database: Separate from production                         │
│  API Keys: Test/sandbox credentials                         │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                     PRODUCTION                               │
│  Real users, real data                                       │
│  Database: Production                                        │
│  API Keys: Live credentials                                  │
└─────────────────────────────────────────────────────────────┘

Principles

1. Never Commit Secrets

Secrets don't belong in version control. Ever.

The mistake: "I'll just commit the API key for now and change it later." You forget. The key is in git history forever.

The principle: Secrets go in environment variables, set outside your codebase. Even on your local machine.

What counts as a secret:

  • API keys and tokens
  • Database passwords
  • Encryption keys
  • OAuth client secrets
  • Webhook signing secrets

Not secrets (okay to commit):

  • Configuration templates with placeholder values
  • Non-sensitive defaults
  • Public API URLs

2. Document, Don't Assume

New team members shouldn't have to guess what variables are needed.

The mistake: The app fails on startup with a cryptic error because a required variable is missing. Nobody documented it.

The principle: Maintain a template file (.env.example) that lists every variable, with descriptions and example values. Keep it updated.

What to include:

  • Variable name
  • Description of what it does
  • Example value (not a real secret)
  • Whether it's required or optional
  • Where to get a valid value

3. Validate Early

Catch configuration problems at startup, not at 3 AM when that code path runs.

The mistake: A typo in a variable name goes unnoticed for weeks. Then a critical feature fails because DATABASE_URL was misspelled.

The principle: Check all required configuration when the application starts. Fail fast with clear error messages.

What to validate:

  • Required variables exist
  • Values are the right type (URLs are URLs, numbers are numbers)
  • Connections work (can you reach the database?)

4. Separate Concerns

Different types of configuration have different needs.

The categories:

TypeExampleChangesSecurity
SecretsAPI keysRarelyHigh
EnvironmentDatabase URLPer environmentMedium
Feature flagsEnable new UIFrequentlyLow
ConstantsRetry countRarelyLow

The principle: Don't treat all configuration the same. Secrets need protection. Feature flags need easy changes. Constants can be in code.

5. Parity Matters

Development should be as close to production as possible.

The mistake: Everything works locally with SQLite. Production uses PostgreSQL. You discover the incompatibility during deployment.

The principle: Use the same services locally that you use in production. If production uses PostgreSQL, run PostgreSQL locally (Docker makes this easy).

Why it matters: Every difference between environments is a potential bug that only appears in production.

Decision Framework

"Should this be an environment variable?"

Yes, make it a variable when:

  • It changes between environments
  • It's a secret
  • You want to change it without code changes
  • It's a connection string or URL

No, keep it in code when:

  • It never changes across environments
  • It's not sensitive
  • It's a constant that defines behavior (retry counts, timeouts)

The test: Would you want to change this without deploying new code? If yes, make it a variable.

"Which file should this go in?"

.env.example (committed):

  • Template listing all variables
  • Placeholder or example values
  • Documentation

.env.local (not committed):

  • Your local overrides
  • Your personal API keys
  • Development-specific settings

.env (sometimes committed):

  • Non-sensitive defaults
  • Shared development configuration

Platform environment (for production):

  • All secrets
  • Production URLs
  • Production-specific settings

"How should I organize environment variables?"

Group by purpose:

  • Database configuration
  • Authentication
  • External APIs
  • Feature flags
  • URLs

Use consistent naming:

  • Prefix related variables: DATABASE_URL, DATABASE_POOL_SIZE
  • Use SCREAMING_SNAKE_CASE
  • Be descriptive: STRIPE_SECRET_KEY not SK

Add comments in your template:

  • Explain non-obvious variables
  • Note where to get values
  • Mark required vs optional

"What should I do when a variable is missing?"

For required variables:

  • Fail at startup with a clear error
  • Name the variable that's missing
  • Suggest how to fix it

For optional variables:

  • Use sensible defaults
  • Document the default behavior
  • Log when falling back to defaults

Never:

  • Silently use an empty string
  • Let the app start and fail later
  • Swallow the error

"How do I handle different databases per environment?"

Common pattern:

EnvironmentDatabaseSet via
DevelopmentLocal SQLite or Docker PostgreSQL.env.local or default
StagingCloud PostgreSQL (separate)Platform environment
ProductionCloud PostgreSQL (production)Platform environment

The principle: If DATABASE_URL is empty, use a local fallback. If set, use the provided connection.

Why separate staging and production: Staging is for testing. You don't want test data in your production database, and you don't want to accidentally break production while testing.

Common Mistakes

Hardcoded Configuration

Values that should be configurable are embedded in code.

Signs: You need to change code and redeploy to point to a different API. Different branches exist just for different environments.

The fix: Extract configurable values to environment variables. Make the code environment-agnostic.

Missing Template

No documentation of what variables are needed.

Signs: Onboarding a new developer takes hours of "what variables do I need?" New deployments fail with missing config.

The fix: Create and maintain .env.example. Update it when you add or remove variables.

Secrets in Git

Sensitive values committed to the repository.

Signs: .env files in git history. API keys visible in the codebase. Secret rotation is a nightmare.

The fix: Add .env.local and similar files to .gitignore. If secrets were committed, rotate them immediately. Consider using tools that scan for leaked secrets.

Environment-Specific Code

Different code paths based on environment name.

Signs: if (process.env.NODE_ENV === 'production') scattered everywhere. Bugs that only appear in staging. Features that work differently in prod.

The fix: Configure behavior via environment variables, not environment names. If production needs different behavior, that behavior is a configuration option.

Missing Validation

App starts with invalid configuration.

Signs: Runtime errors from missing variables. Features silently fail. Hard-to-debug production issues.

The fix: Validate all configuration at startup. Fail fast with clear messages.

Local and Production Drift

Your local setup is nothing like production.

Signs: "Works on my machine." Bugs that only appear in production. Surprised by behavior differences.

The fix: Use the same database, same services, same setup locally. Docker makes this manageable.

How to Evaluate Your Environment Setup

Your setup is working if:

  • New developers can get running with clear instructions
  • Secrets are not in version control
  • Missing configuration fails at startup with helpful errors
  • You can deploy to any environment without code changes
  • Local development resembles production

Your setup needs work if:

  • Onboarding requires tribal knowledge
  • Developers share API keys via chat
  • Environment issues are discovered in production
  • Different environments require different code
  • You're not sure what's configured in production

Environment Setup Checklist

For New Projects

  1. Create .env.example with all variables documented
  2. Add .env.local and .env*.local to .gitignore
  3. Set up validation at startup
  4. Document setup in README
  5. Configure production secrets in your deployment platform

For New Team Members

  1. Copy .env.example to .env.local
  2. Fill in required values (get from team lead or password manager)
  3. Install local dependencies (database, etc.)
  4. Run and verify the app starts

For Adding New Variables

  1. Add to .env.example with description
  2. Add to your .env.local
  3. Add validation if required
  4. Add to production environment
  5. Deploy and verify

The Secret Management Hierarchy

From least secure to most secure:

  1. Plain text files (never for secrets in production)
  2. Environment variables (minimum acceptable for secrets)
  3. Encrypted at rest (platform secrets management)
  4. Secrets management service (Vault, AWS Secrets Manager)
  5. Hardware security modules (for the most sensitive keys)

The principle: Match the security level to the sensitivity. Most apps are fine with platform environment variables. Financial or health data may need more.

Quick Reference

PrincipleImplementation
Never commit secrets.gitignore, platform env
Document everything.env.example
Validate at startupFail fast on missing config
Same code, different configEnvironment variables
Development = ProductionDocker, same services