Environment Variables vs Remote Configuration: When to Use Which
“Just put it in an environment variable” is common advice. And often it’s correct—environment variables are simple, secure, and universally supported. But sometimes they’re the wrong tool.
In this article, we’ll compare environment variables with remote configuration and establish clear guidelines for when to use each.
How environment variables work
Environment variables are key-value pairs available to a process:
const databaseUrl = process.env.DATABASE_URL
const apiKey = process.env.API_KEY
const logLevel = process.env.LOG_LEVEL ?? 'INFO'They’re set outside your code—in shell scripts, Docker Compose files, or cloud platform settings:
# docker-compose.yml
services:
app:
environment:
- DATABASE_URL=postgresql://localhost/mydb
- LOG_LEVEL=DEBUGEnvironment variables are:
- Read once at process start
- Static for the lifetime of the process
- Scoped to a single process (and its children)
How remote configuration works
Remote configuration fetches values from an external server:
import { Replane } from '@replanejs/sdk'
interface Configs {
'rate-limit': number
'new-feature-enabled': boolean
}
const replane = new Replane<Configs>()
await replane.connect({
sdkKey: process.env.REPLANE_SDK_KEY!,
baseUrl: 'https://cloud.replane.dev',
})
const rateLimit = replane.get('rate-limit')
const featureEnabled = replane.get('new-feature-enabled')Remote configuration is:
- Read at runtime (continuously updated)
- Dynamic (changes without restart)
- Shared across all instances
What’s the key difference between environment variables and remote configuration?
When to use environment variables
Environment variables are the right choice for:
Secrets and credentials
API keys, database passwords, and encryption keys belong in environment variables (or a secrets manager):
// Good: secrets in env vars
const stripeKey = process.env.STRIPE_API_KEY
const dbPassword = process.env.DATABASE_PASSWORDWhy? Secrets are sensitive and should:
- Never appear in dashboards or logs
- Be rotated through secure channels
- Have strict access controls
Remote config systems aren’t designed for secret management.
Connection strings and endpoints
Database URLs and service endpoints rarely change at runtime:
// Good: connection info in env vars
const databaseUrl = process.env.DATABASE_URL
const redisUrl = process.env.REDIS_URL
const apiGateway = process.env.API_GATEWAY_URLThese values are tied to infrastructure. When they change, you typically need to restart connections anyway.
Environment-specific settings
Settings that differ between dev/staging/production but don’t change within an environment:
// Good: environment config in env vars
const environment = process.env.ENVIRONMENT ?? 'development'
const debugMode = process.env.DEBUG === 'true'Build-time configuration
Settings needed at build time (not runtime):
# Build with specific settings
NEXT_PUBLIC_API_URL=https://api.example.com npm run buildWhen to use remote configuration
Remote configuration is the right choice for:
Feature flags
Toggles that need to change instantly without deploys:
// Good: feature flags in remote config
if (replane.get('new-checkout-enabled')) {
showNewCheckout()
}You want to enable a feature for 10% of users, watch metrics, then increase to 100%—all without deploying.
Operational parameters
Settings you tune based on real-world conditions:
// Good: operational tuning in remote config
const rateLimit = replane.get('api-rate-limit')
const cacheTtl = replane.get('cache-ttl-seconds')
const batchSize = replane.get('worker-batch-size')During a traffic spike, you might need to lower rate limits. During a database slowdown, you might increase timeouts. Remote config lets you respond in seconds.
Kill switches
Emergency controls to disable features:
// Good: kill switch in remote config
if (replane.get('payments-enabled', { default: true })) {
processPayment()
} else {
showMaintenanceMessage()
}When payments break at 2am, you flip a switch instead of emergency-deploying.
Per-user or per-tenant settings
Different customers get different values:
// Good: per-tenant config in remote config
const maxUsers = replane.get('max-users', {
context: { tenant: tenant.id }
})Enterprise customers might get 10,000 users while free tier gets 10.
Where should you store your database connection password?
Decision framework
Here’s a simple flowchart for deciding where to put configuration:
Is it a secret? ──────Yes──────► Environment variable
│ (or secrets manager)
No
▼
Does it need to change ──Yes──► Remote configuration
without restarting?
│
No
▼
Environment variableOr as a table:
| Configuration Type | Where to Store | |-------------------|----------------| | API keys, passwords | Env var / Secrets manager | | Database URLs | Environment variable | | Feature flags | Remote config | | Rate limits, timeouts | Remote config | | Kill switches | Remote config | | Build-time settings | Environment variable | | Per-user settings | Remote config |
Combining both approaches
In practice, you’ll use both. Here’s a typical pattern:
import { Replane } from '@replanejs/sdk'
// Static config from environment (secrets, connections)
const DATABASE_URL = process.env.DATABASE_URL!
const STRIPE_KEY = process.env.STRIPE_API_KEY!
const ENVIRONMENT = process.env.ENVIRONMENT ?? 'development'
interface Configs {
'rate-limit': number
'feature-enabled': boolean
}
// The SDK key itself comes from env var
const replane = new Replane<Configs>()
await replane.connect({
sdkKey: process.env.REPLANE_SDK_KEY!,
baseUrl: 'https://cloud.replane.dev',
})
// Dynamic config from remote (features, tuning)
function getRateLimit(): number {
return replane.get('rate-limit', { default: 100 })
}
function isFeatureEnabled(name: keyof Configs, userContext?: Record<string, unknown>): boolean {
if (userContext) {
return replane.get(name, { context: userContext, default: false }) as boolean
}
return replane.get(name, { default: false }) as boolean
}Notice that even the remote config SDK key comes from an environment variable—you use env vars for the truly static, secret values.
Common mistakes
Putting secrets in remote config
// Bad: secrets in remote config
const stripeKey = replane.get('stripe-api-key')Remote config dashboards show values in plain text. Logs might capture them. Use env vars or a secrets manager for secrets.
Putting everything in env vars
// Bad: dynamic values as env vars require restarts
const FEATURE_NEW_CHECKOUT = process.env.FEATURE_NEW_CHECKOUT === 'true'
const RATE_LIMIT = parseInt(process.env.RATE_LIMIT ?? '100', 10)If you’re frequently changing these values and restarting to pick up changes, they should be in remote config.
No defaults for remote config
// Bad: throws if config server unavailable
const rateLimit = replane.get('rate-limit')
// Good: has default
const rateLimit = replane.get('rate-limit', { default: 100 })Your app should work (perhaps degraded) even if remote config is temporarily unavailable.
Tools for remote configuration
If you decide to use remote configuration, here are some options:
- Replane — Open-source, real-time updates via SSE, self-hosted or cloud
- LaunchDarkly — Enterprise feature flags
- AWS AppConfig — Managed by AWS, good for AWS-heavy environments
- Consul — Service mesh with KV store
For self-hosting and open-source, Replane is worth considering—it handles versioning, rollback, and has SDKs for multiple languages.
Summary
Use environment variables for:
- Secrets and credentials
- Connection strings and endpoints
- Environment-specific (dev/staging/prod) settings
- Build-time configuration
Use remote configuration for:
- Feature flags
- Operational tuning (rate limits, timeouts)
- Kill switches
- Per-user or per-tenant settings
The key question: “Does this need to change without restarting the application?” If yes, use remote config. If no, environment variables are simpler.
Exercises
-
Audit your configuration: Look at an application you’ve worked on. Categorize each config value as “should be env var” or “should be remote config”.
-
Migrate a value: Take one value that’s currently in an environment variable but should be dynamic, and implement it using remote configuration.
-
Design a config architecture: For a new application, design the configuration strategy. What goes in env vars? What goes in remote config? Document your decisions.
Discussion