Less Env, More Config

Env vars are easy - which is exactly how every product knob quietly becomes one.

Recently, I was cleaning up configuration in one of my own apps and ran into one of those tiny engineering smells that doesn’t look serious until you stare at it for a while.

Too many things had become environment variables.

Not broken. Not dangerous on day one. Just… noisy.

The thing I keep coming back to -

Environment variables are great for boot-time secrets and deployment invariants.
They are not a great home for every operational knob a product might need to change.

It starts… innocently. A value needs to change without touching code, so it goes into an env. Then another one. Then a provider toggle. Then a limit. And because env vars are easy, the answer keeps becoming "just make it an env" - until a deployment surface has quietly become a control panel.

Every knob ends up on the same path:

You want to… The answer
Change a user-facing limit Update an env, redeploy
Disable one provider Env
Rotate one small workflow’s value Env again
Know who changed it last time Good luck, hope the dashboard has audit logs. 🙃

Where it gets messy

  • Visibility.
    A setting in the app layer can be shown, searched, validated, even given a small Admin UI. As an env var, its source of truth is split between a hosting dashboard, CI variables, .env.example, and someone’s memory.

  • Deploy coupling.
    Operational values change because the product is changing, not because the release is. Restarting a process to tweak them is fine twice, then it’s friction people work around.

  • Drift.
    Staging gets one value, production another, local has an old copy, and a feature works on one machine because a forgotten .env file still has something the platform does not.

Nothing explodes in a fun way. It just wastes time.


The better question

Not “can this be an env?” - most things can. The question is:

“Does this value bootstrap the app, or does it describe how the app should behave?”

If the app can’t safely talk to its own storage without it, that’s bootstrap. Everything past that line is behavior.

That split cleans things up fast:

Kind Where it belongs
Runtime wiring Env vars
Root secrets / KEKs Env vars or a managed secret store
App behavior settings Database / platform settings table
Tenant-specific config Database, scoped by tenant/project
Sensitive app settings Database, encrypted with key versioning
Values edited by admins Database, with audit/history if important

Not because databases are magically cleaner - you can absolutely make a mess there too. But behavior settings act like app data, so they belong where the app can own validation, history, and permissions. The storage does not have to be fancy; a small settings table is enough for a lot of apps. The important part is not the tool, it’s the boundary.

You might ask - Doesn't 12-Factor say config goes in the env?

It does - and Factor III defines config as everything that is likely to vary between deploys. A feature flag or a per-tenant limit doesn’t vary between deploys; it varies between Tuesdays. The split above isn’t breaking the rule, it’s reading it fully.


But some of it is sensitive

That’s the part that makes people reach for env vars again. Fair - but sensitive does not automatically mean “must be a separate env var”.

Keep one master key - a KEKKey Encryption Key - a root key that only encrypts other keys and secrets. in the environment, store the sensitive settings encrypted in the database, decrypt only where needed. Each record tracks its key version, so rotation is just: add a new key, re-encrypt gradually. The environment still protects the thing that matters most, and there’s exactly one value to guard, audit, and rotate - instead of twenty sensitive-ish knobs scattered across deployment config.


Final thought

  • If a value is needed to start the app, keep it close to the process.
  • If a value describes how the app behaves, store it where the app can own it.
  • If the only reason something is an env var is "because config goes in env", it probably deserves another look.