Back to Blog
Guide9 min read2026-07-08

Twelve-Factor Config in Practice

The Twelve-Factor App's config principle — strict separation of config from code via environment variables — is deceptively simple. Here's how to apply it correctly, the common mistakes, and patterns that scale.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Of the twelve factors in the Twelve-Factor App methodology, config (Factor III) is the one most teams half-implement and then quietly suffer for. The principle sounds trivial — "store config in the environment" — but applying it well prevents a whole category of secrets leaks and deployment pain. This guide covers what it really means and how to do it right.

The principle

Factor III states: strictly separate config from code. Config is anything that varies between deploys (dev, staging, production): database URLs, API keys, credentials, hostnames, feature flags, resource handles. Code is everything that *doesn't* change between deploys.

The litmus test the methodology offers is sharp: could you open-source your codebase right now without exposing any secrets? If not, you have config baked into code. That's the violation.

Why config doesn't belong in code

When config lives in code, several things break:

  • Secrets leak. A committed API key is in your Git history forever, visible to anyone with repo access.
  • You need a rebuild per environment. Different config.prod.js vs config.dev.js means the artifact isn't portable — you can't promote the *same* build from staging to prod.
  • Rotation is painful. Changing a credential means a code change and redeploy instead of an env-var update.

The goal is one immutable artifact that runs anywhere, with behavior determined entirely by the environment it's dropped into.

The right way: environment variables

Twelve-Factor prescribes environment variables as the config store. They're language-agnostic, available everywhere, and never accidentally committed (they live outside the codebase).

// Read config from the environment
const config = {
  databaseUrl: process.env.DATABASE_URL,
  port: parseInt(process.env.PORT ?? '8080', 10),
  logLevel: process.env.LOG_LEVEL ?? 'info',
  stripeKey: process.env.STRIPE_SECRET_KEY,
};

Notice the pattern: read from process.env, provide sensible defaults for non-secret values, and never hardcode a secret.

Common mistakes (and fixes)

Mistake 1: Grouped "environment" config files

The anti-pattern Twelve-Factor explicitly warns against: config/development.rb, config/production.rb. These don't scale — every new deploy target needs a new file, and they tend to accumulate secrets. Fix: individual environment variables, fully orthogonal to each other.

Mistake 2: Committing .env files

.env files are great for local development — but they must never be committed. Add .env to .gitignore on day one. Commit a .env.example with the *names* (not values) so teammates know what's needed:

# .env.example (committed, no real values)
DATABASE_URL=
STRIPE_SECRET_KEY=
LOG_LEVEL=info

Mistake 3: Build-time vs runtime confusion

A subtle one, especially for frontends. Static-site builds *inline* env vars at build time (VITE_*, NEXT_PUBLIC_*). Anything inlined into client JS is public — never put a secret there. Backend config, by contrast, is read at runtime and stays server-side. Know which kind each variable is.

Config typeWhen readSecret-safe?
Frontend (VITE_*, NEXT_PUBLIC_*)Build time, inlined into JSNo — public
Backend runtime envRuntime, server-sideYes

Mistake 4: No validation

A missing env var often fails silently or deep in a request, far from the cause. Fix: validate config at startup and fail fast:

const required = ['DATABASE_URL', 'STRIPE_SECRET_KEY'];
for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Missing required env var: ${key}`);
  }
}

Crashing immediately with a clear message beats a confusing 500 an hour later.

Config and the immutable artifact

The payoff of doing this right ties into the deploy pipeline. When config is fully externalized, the image you build is environment-agnostic — the exact bytes you tested in staging are what you promote to production, with only the env vars differing. That's what makes promotion safe and rollbacks trustworthy. Config-in-code breaks this by coupling the artifact to one environment.

Secrets deserve extra care

Not all config is equal. Non-secret config (log level, feature flags) can be plain env vars. Secrets (DB passwords, API keys) warrant:

  • Storage in a secret manager or your platform's secret env vars, not plaintext files.
  • Regular rotation — easy when it's just an env-var change.
  • Never logging them (don't console.log(config) wholesale).
  • Least-privilege scope.

How PandaStack applies this

PandaStack is built around Twelve-Factor config:

  • Env vars are first-class — set per service in the dashboard, separate from your code, with secret values kept out of the repo.
  • DATABASE_URL is auto-injected when you attach a managed database. This is Factor III done for you: the database is a *backing service* (Factor IV) referenced by a URL in the environment, so you can swap or move the database without touching code.
  • The same image runs across deploys — config differences live in the environment, so promotion and rollback are clean.

The auto-wired DATABASE_URL is a nice illustration of the principle in action: your code reads process.env.DATABASE_URL and doesn't care *which* database it is or *where* it lives — the environment supplies that.

Quick checklist

  • [ ] No secrets in the codebase (could you open-source it today?)
  • [ ] .env in .gitignore; .env.example committed
  • [ ] All config read from env vars with sensible defaults for non-secrets
  • [ ] Required vars validated at startup (fail fast)
  • [ ] Frontend build-time vars contain no secrets
  • [ ] One artifact, many environments
  • [ ] Secrets stored in a secret store and rotatable

References

  • [The Twelve-Factor App: III. Config](https://12factor.net/config)
  • [The Twelve-Factor App: IV. Backing services](https://12factor.net/backing-services)
  • [OWASP: Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
  • [Vite: env variables and modes](https://vitejs.dev/guide/env-and-mode.html)
  • [12factor.net (full methodology)](https://12factor.net/)

---

PandaStack handles the hardest part of Twelve-Factor config for you: attach a managed database and DATABASE_URL is injected automatically, with env vars managed per service and kept out of your repo. Try it on the free tier at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Guide

Browse all Guide articles →

See also