Feature Flags vs Environment Variables: When to Use Each

Supa DeveloperSupa Developer
··6 min read

Environment variables and feature flags both put a condition in your code that changes behaviour without a code edit. That surface-level similarity causes a lot of teams to treat them as interchangeable, and they run into problems that are entirely avoidable.

They're not the same tool. They solve different problems, at different points in the deployment lifecycle, with different tradeoffs. Using the wrong one creates friction that compounds over time.

This guide draws a clear line between them.


What environment variables actually are

An environment variable is a value injected into your process at startup time by the host environment: your OS, your CI runner, your container orchestrator, or your deployment platform.

# .env.production
DATABASE_URL=postgres://prod-db:5432/myapp
STRIPE_SECRET_KEY=sk_live_...
NEXT_PUBLIC_API_BASE_URL=https://api.myapp.com
SENTRY_DSN=https://abc123@sentry.io/456

The critical word is startup. Your app reads these values once when the process boots. Changing them requires restarting the process, which in most deployment pipelines means a new deployment.

Environment variables are for configuration that:

  • Is stable for the lifetime of a running process
  • Varies between environments (development, staging, production), not between users
  • Contains secrets or infrastructure references (API keys, database URLs, service endpoints)
  • Is known at deploy time

What feature flags actually are

A feature flag is a condition evaluated at request time against values stored in an external system. The decision can depend on who is asking, when they're asking, and what percentage of traffic should see each variant.

// Evaluated fresh on every request (no restart needed)
const showNewCheckout = await client.getFeature('checkout.redesign', {
  userId: user.id,
  plan: user.plan,
  email: user.email,
})

The flag value comes from a service (like Supaship) that you can update from a dashboard. The change takes effect for the next request with no deployment, no restart, and no code change.

Feature flags are for decisions that:

  • Need to change without a deployment
  • Vary per user, segment, or percentage of traffic
  • Are temporary (rollouts, experiments, kill switches)
  • Need to be reversed instantly if something goes wrong

The key differences

DimensionEnvironment variableFeature flag
Evaluated atProcess startupRequest time
Changes take effectAfter redeploymentInstantly, no restart
Varies byEnvironment (dev/staging/prod)User, segment, percentage, plan
Best forSecrets, infra config, API keysRollouts, experiments, kill switches
LifespanPermanent (for that environment)Temporary; cleaned up after rollout
VisibilityBaked into the processDashboard, audit log, change history
Rollback speedMinutes (new deployment)Seconds (dashboard toggle)
User targetingNoYes

Where teams go wrong

Using env vars as feature flags

This is the most common mistake. It looks like this:

# .env.production
ENABLE_NEW_DASHBOARD=true
ENABLE_BETA_SEARCH=false
ENABLE_AI_ASSISTANT=true
if (process.env.ENABLE_NEW_DASHBOARD === 'true') {
  return <NewDashboard />
}

It works, until it doesn't. Problems that accumulate:

Changing a value requires a deployment. If the new dashboard has a bug at 11pm, disabling it means writing a PR, merging, waiting for CI, and deploying. That's a 15–30 minute incident extension at minimum. A feature flag is a dashboard toggle you can hit in 10 seconds.

There's no per-user control. You can't roll out to 5% of users. You can't enable it for your beta testers while keeping it off for everyone else. You can't target users by plan or segment. The flag is either on for all prod traffic or off for all prod traffic, with no middle ground.

There's no audit trail. Who set ENABLE_NEW_DASHBOARD=true? When? Why was it changed? Environment variable changes disappear into deployment history. A feature flag platform records every change with a timestamp and the actor who made it.

Env vars accumulate and rot. Feature rollout env vars never get cleaned up. Three years later the codebase has ENABLE_LEGACY_FLOW_V2=false and no one knows what it does or whether it's safe to remove.

Using feature flags as environment config

The opposite mistake is less common but worth naming. Storing secrets, infrastructure URLs, or values that are stable per environment in a feature flag platform is the wrong tool:

// Don't do this
const dbUrl = await client.getFeature('database-url')
const stripeKey = await client.getFeature('stripe-secret-key')

Feature flags are evaluated per request, so adding a network call to fetch your database URL on every request introduces unnecessary latency and a new failure mode. Secrets don't belong in a flag platform's storage model. And these values don't need per-user targeting or instant rollback. They just need to exist per environment.


The right tool for common scenarios

"I need a different API endpoint in staging vs production"

Environment variable. This is exactly what env vars are for: stable, environment-specific config.

# .env.staging
NEXT_PUBLIC_API_URL=https://api-staging.myapp.com

# .env.production
NEXT_PUBLIC_API_URL=https://api.myapp.com

"I'm launching a redesigned onboarding flow and want to roll it out to 10% of new users"

Feature flag. You need per-user control, a gradual rollout, and the ability to kill it instantly if something's wrong.

const newOnboarding = await client.getFeature('onboarding.redesign-v3', {
  userId: user.id,
  createdAt: user.createdAt,
})

"I want to enable a feature only for paying customers on the Pro plan"

Feature flag. This is attribute-based targeting, a core feature flag use case that env vars cannot do.

const aiFeatures = await client.getFeature('features.ai-assistant', {
  userId: user.id,
  plan: user.plan, // targeting rule: plan === 'pro' || plan === 'enterprise'
})

"I have a Stripe webhook secret that needs to be in production"

Environment variable. Secrets are env vars. Full stop.

STRIPE_WEBHOOK_SECRET=whsec_...

"The new payment provider integration is unstable and I want a kill switch"

Feature flag. A kill switch needs to be actionable in seconds during an incident, not minutes after a deployment.

const useNewPaymentProvider = await client.getFeature(
  'payments.provider-v2.enabled',
)
const provider = useNewPaymentProvider ? newProvider : legacyProvider

"I'm building a dark mode toggle that users control themselves"

Neither. That's a user preference stored in your database or localStorage. Feature flags control what the app makes available, not what individual users choose within it.


Using both together: the common pattern

In practice, the two tools complement each other. Environment variables handle the stable, deployment-time configuration. Feature flags handle the runtime, per-user decisions.

// lib/features.ts
import { SupaClient } from '@supashiphq/javascript-sdk'

// Environment variable: stable API key, set at deploy time
export const featureClient = new SupaClient({
  apiKey: process.env.SUPASHIP_API_KEY!, // ← env var: infrastructure config
  environment: process.env.NODE_ENV!, // ← env var: environment name
  features: FEATURE_FLAGS,
  context: {},
})
// In a request handler
const features = await featureClient.getFeatures(
  ['new-checkout', 'ai-assistant', 'beta-search'],
  {
    userId: user.id, // ← feature flag: per-user evaluation
    plan: user.plan,
  },
)

The API key that connects to Supaship is an environment variable. What the SDK returns (which features this specific user sees) is determined by feature flags.


A simple decision rule

Before adding a configuration value, ask two questions:

  1. Does it vary between users or traffic segments?
    If yes → feature flag.

  2. Does it need to change without a deployment or restart?
    If yes → feature flag.

If the answer to both is no (it's stable per environment and set at deploy time), use an environment variable.


Feature flags and environment variables are both levers, but they operate at different levels of the stack. Using each for what it's designed for keeps your codebase clean, your incidents manageable, and your team able to ship with confidence.

Ready to replace your feature-flag env vars with a proper platform? Sign up for Supaship free · 1M events/month, per-user targeting, instant rollbacks, and an audit log. Pro plan is $30/month for your entire workspace.

Related reading:

Framework guides: Next.js · React · Vue · Node.js


Feedback

Got thoughts on this?

We're constantly learning how developers actually use these tools. Ideas, use cases, integration requests — every bit of feedback makes the platform better for everyone.

Thanks for being part of the journey — Supaship