TS-48: Environment variables

This short technical standard covers the use of environment variables in applications.

What counts as config

Not every value that varies is config, and not every setting belongs in an environment variable. Config, in the sense this standard uses the term, is anything that is likely to vary between deploys — development, staging, production, and any per-developer or per-instance deployment. This includes database URLs, credentials for backing services, hostnames for external services, and per-deploy values such as the canonical domain name.

It does NOT include internal application settings that never vary between deploys — routing rules, the choice of templating engine, internal module wiring, or similar decisions that are part of the application’s design rather than its deployment. Config that does not vary between deploys does not need to be, and SHOULD NOT be, externalized into an environment variable: doing so adds indirection without adding flexibility, and expands the set of variables an operator has to understand and manage at deploy time.

A simple test for whether a value is config: if the codebase could be made open source at any moment without compromising any credentials, it is correctly factored. Values that would leak a secret, an internal hostname, or any other deploy-specific detail if the repository went public MUST be externalized as environment variables; everything else stays in code.

Core requirements

Environment-specific configuration MUST be stored in environment variables.

Storing config in environment variables, rather than in config files or language-level constants, is a deliberate choice: environment variables are easy to change between deploys without changing any code, there is little chance of them being checked into the code repository by accident, and they are a language- and operating-system-agnostic mechanism understood by every runtime and every deployment platform.

Applications SHOULD provide sensible defaults for environment configuration options, except those that store security credentials – these MUST be empty by default. The purpose is to simplify environment configuration.

Configuration defaults SHOULD be set and optimized for the production environment. The purpose of this constraint is to reduce risks associated with missing configuration in the production environment.

Environment configuration MUST be treated as external input to an application, and therefore values MUST be validated and sanitized before use. Tools such as Pkl can be helpful for defining configuration schemas and for validating environment configurations against the schemas.

Verifying correct separation

A practical test for whether config has been correctly separated from code: the codebase could be made open source at any moment, without compromising any credentials. If publishing the repository publicly today would leak a database password, an API key, or any other secret, some config remains embedded in code and MUST be moved into an environment variable.

Granularity

Environment variables MUST be managed as independent, orthogonal controls, one per configuration concern, rather than grouped into named collections such as STAGING_DATABASE_URL and PRODUCTION_DATABASE_URL. The correct pattern is a single, plainly-named variable — DATABASE_URL — whose value differs per deploy. Combinatorial naming schemes do not scale: every additional deploy target multiplies the number of variables an operator has to define, and nothing prevents a variable from being defined for one deploy and forgotten for another.

This granularity principle is a different concern from the named deployment environments (development, testing, staging, production) described in TS-9: Version control. TS-9’s environments describe which branches deploy to which running instances of the software; the granularity principle described here concerns how individual configuration variables are scoped and named within any one of those instances. The two are complementary, not in conflict: a deploy to the staging environment and a deploy to the production environment each read the same plainly-named DATABASE_URL variable, populated with a different value for each deploy target.


References