---
name: twelve-factor-config
source: https://app.decimal.ai/s/twelve-factor-config@1/SKILL.md
source_sha256: 70fc2b42291b
---

# Twelve-Factor config (factor III)

## Contract

Anything that changes between one deploy and the next is stored in its own environment variable, read at
runtime. Nothing that varies between deploys is a code constant, a committed file with secrets, or a
named-environment switch. Apply this to how an app obtains configuration and credentials; it does not
cover the other twelve factors.

## Rules

1. **Deploy-varying config → an environment variable.** A value that differs between the laptop, CI, and
   each production deploy (a database URL, an API key, a hostname, a log level) is read from an
   environment variable at runtime. This is the whole rule; everything below is a consequence.

2. **No credentials in code.** A secret, key, password, or connection string is never a literal in a
   source file. `os.environ["DATABASE_URL"]`, not `DATABASE_URL = "postgres://…"`.

3. **No committed config file holding secrets.** Do not check a `config.py`, `secrets.yaml`, or
   `config.json` full of credentials into version control. The environment supplies them; the repo does
   not.

4. **Granular and independent.** Each value is its own variable, orthogonal to the others — `DATABASE_URL`,
   `REDIS_URL`, `STRIPE_KEY` as separate variables. Not one packed blob, not values that only make sense
   together as a set.

5. **Never grouped into named environments.** Do not define `DevelopmentConfig` / `StagingConfig` /
   `ProductionConfig` classes, per-tier files, or `if ENV == "production":` switch blocks that select a
   bundle of values by an environment name. Each deploy sets its own variables directly; there is no
   named group to switch on. Adding a new deploy must need only new variable values, never a new class,
   file, or branch.

6. **The litmus.** Could this codebase be made open source right now, this minute, without exposing any
   credential? If a checked-in file would leak a secret, it fails factor III.

7. **Truly constant values stay in code.** A value that is the *same in every deploy* — a fixed page
   size, a protocol constant, a timeout that never varies — is an ordinary code constant, not an
   environment variable. Factor III moves only what *varies between deploys*; it does not push invariant
   constants or app internals into the environment.

## Worked examples

The base's default is on the left; the conforming form on the right.

Hard-coded connection string → environment variable:

```python
# BEFORE
DATABASE_URL = "postgres://app:s3cret@db.internal:5432/app"

# AFTER
DATABASE_URL = os.environ["DATABASE_URL"]   # set per deploy; no default, absence fails startup
```

A committed secrets file → read from the environment instead:

```python
# BEFORE  (config.py, committed)
STRIPE_KEY = "sk_live_51H..."
SENDGRID_KEY = "SG.xxxx"

# AFTER
STRIPE_KEY = os.environ["STRIPE_KEY"]
SENDGRID_KEY = os.environ["SENDGRID_KEY"]
```

The named-environment class ladder → flat, per-value variables:

```python
# BEFORE
class ProductionConfig:
    DB = "postgres://prod…"
    DEBUG = False
class StagingConfig:
    DB = "postgres://stg…"
    DEBUG = True
CONFIG = ProductionConfig if os.environ["APP_ENV"] == "prod" else StagingConfig

# AFTER
DB = os.environ["DATABASE_URL"]
DEBUG = os.environ.get("DEBUG", "false") == "true"
# no APP_ENV switch; each deploy sets DATABASE_URL and DEBUG itself
```

A value that never varies stays a constant — do *not* environment-ize it:

```python
# CORRECT (unchanged)
MAX_UPLOAD_BYTES = 10_485_760   # the same in every deploy → a plain constant, not an env var
```

## Edge cases & exceptions

- **A local `.env` for developer convenience** → fine *only* if gitignored and the code still reads from
  the process environment (`.env` populates it); the code must not parse a committed file as its source
  of truth.
- **Missing required variable** → fail fast at startup with a clear error. Do not fall back to a
  hard-coded default secret.
- **Non-secret but deploy-varying** (a public hostname, a feature toggle, a log level) → still an
  environment variable; "varies between deploys" is the test, not "is it secret."
- **Constant that is the same everywhere** (page size, retry cap that never changes, a wire-protocol
  magic number) → keep it a code constant; moving it to the environment is over-config.
- **A rendered config file from an orchestrator** → the *app itself* should read the environment, not
  parse a generated file full of secrets; injecting each value as a variable is the conforming path.

## Do / Don't

- Do read deploy-varying values from the environment. Don't hard-code them in source.
- Do keep each value in its own variable. Don't pack them into one blob or a shared bundle.
- Do let each deploy set its own variables. Don't switch a named group with `if ENV == …`.
- Do fail startup on a missing secret. Don't ship a fallback default credential in code.
- Do keep invariant constants in code. Don't environment-ize a value that never varies between deploys.
- Do keep the repo credential-free. Don't commit a config file that holds secrets.

## Common mistakes

- A hard-coded connection string or API key sitting in a source file.
- A committed `config.py` / `secrets.yaml` carrying real credentials.
- `DevelopmentConfig` / `ProductionConfig` classes, or `if env == "production":` blocks, grouping config
  by a named environment.
- One giant serialized config object instead of granular, independent variables.
- Environment-izing values that are identical in every deploy (over-config).
- A default secret baked in as a fallback when the variable is unset.

## Quick checklist

- Every deploy-varying value is read from its own environment variable.
- No credential is a literal in code or in any committed file.
- No `DevelopmentConfig`/`ProductionConfig` class, per-tier file, or `ENV ==` switch block.
- Missing required variable fails startup — no fallback default secret.
- Invariant values stay code constants, not environment variables.
- The repo could be open-sourced now without leaking a credential.
