// blog/developer/
Back to Blog
Developer · Published June 18, 2026 · 8 min read · By Toine ·

Update note: Rewritten from test environment experience; dead generator link replaced with the .env.example generator, Node's built-in --env-file added, UUID RFC updated to 9562

Environment Variables and .env Files: The Variable That Still Points at Production

Environment Variables and .env Files: The Variable That Still Points at Production

The configuration defect I worry about most in a test environment is not a missing value. It is a value that is there, valid, and pointing at production: a payment endpoint, a mail server, a supplier's live interface. Everything works in the test run, and something real happens on the other end. That is an environment variable problem, and it is the reason I care about a file most people treat as clutter.

Every project starts with two or three variables. By the time it is live there are thirty, spread over .env.local, .env.production, the hosting dashboard and a chat message from six months ago that says "here is the Stripe key". The idea of keeping configuration out of code is sound. The execution is where teams lose keys to git history and evenings to onboarding. This post is the layout I would use, which file is committed and which never is, how the frameworks differ, and what to do the day a .env file ends up in the repository anyway.

* * *

The file, and the four rules that go with it

A .env file is key-value pairs, one per line, read into the process environment at start-up. The dotenv library made the pattern common; most frameworks now read the file themselves.

` # Database DATABASE_URL=postgresql://user:password@localhost:5432/mydb DATABASE_POOL_SIZE=10

# Authentication JWT_SECRET=a-very-long-random-string-here SESSION_TIMEOUT=3600

# External APIs STRIPE_SECRET_KEY=sk_test_... STRIPE_PUBLISHABLE_KEY=pk_test_... SENDGRID_API_KEY=SG... `

  • Never commit .env. Put .env* in .gitignore in the first commit of the project, before there is anything to leak. Removing a secret from git history is possible and unpleasant, and the secret is compromised either way.
  • Do commit .env.example. Same variable names, placeholder values, comments explaining what each one is for. It is the onboarding document. The .env.example generator makes one from a real .env in one paste: it keeps the names, comments and blank lines, blanks the values, and can leave ports, booleans and localhost URLs in place because those are configuration rather than credentials. It runs in the browser; the file with the real values is not uploaded anywhere.
  • Use names that say what the thing is. DB_URL is ambiguous when there are two databases. DATABASE_URL and ANALYTICS_DATABASE_URL are not.
  • Group with comments. Database, auth, external services, feature flags. A thirty-line file with headings is scannable; without them it is a puzzle.

And use real random values even in development. The password generator produces a 64-character random string in one click. "password123" in a development file has a way of surviving into a staging environment, and from there into a screenshot.

Terminal window showing environment variable configuration
Terminal window showing environment variable configuration
* * *

One file per environment, and the secrets are not in any of them

Most frameworks load a chain of files by environment:

` .env # defaults, every environment .env.local # your machine, never committed .env.development # development values .env.production # production values .env.test # values that make tests repeatable `

Later files override earlier ones: .env, then .env.[environment], then .env.local on top. What goes where:

  • .env: non-secret defaults that work for everyone. Ports, feature flags in their development state, the URL of the local mail catcher.
  • .env.local: whatever is specific to your machine, secrets included. Ignored by git, never shared.
  • .env.production: production configuration that is not secret. CDN hostnames, production flags. The production secrets themselves do not live in this file or any file in the repository. They live in the hosting platform's settings, where access is controlled and changes are logged. ToolForte runs on Vercel and that is exactly where its keys are.
  • .env.test: the values that keep a test run deterministic. A test database, a mocked payment endpoint, a fixed seed for anything random.

That last file is the one that prevents the defect from the first paragraph. Where I have influence over a test environment, every external endpoint in it is either a stub or a supplier's own test system, and the check that it is not a production address is part of the entry criteria for the test phase, not something discovered during it.

Identifiers that have to be unique across environments (instance IDs, correlation tokens) can come from the UUID generator; a v4 UUID (RFC 9562, which replaced RFC 4122 in 2024) does not need coordination between machines to be unique.

Key takeaway

Most frameworks load a chain of files by environment: ``` .env # defaults, every environment .env.local # your machine, never committed .env.development # development values .env.production # production values .env.test # values that make tests repeatable ``` Later files override earlier ones: `.env`, then `.env.[environment]`, then `.env.local` on top.

* * *

Five levels, and level two is where most teams stop

A .env file is plain text on disk. Anyone who can read the disk can read it. Environment variables are better than secrets in source code, and that is all they are.

  1. Secrets out of code. The baseline. Whoever sees the source (open source, a leaked repository, a shared screen) does not see the keys.
  2. .env out of git. The .gitignore line, and once in a while git log --all --full-history -- '.env' to confirm nothing ever slipped in. If it did, rotate every value in that file; history is permanent.
  3. Different secrets per environment. The development database password is not the production one. A stolen laptop should not be a production incident.
  4. Access to production secrets limited to the people who need them. Vercel's environment settings, AWS Secrets Manager, Heroku config vars all do this. Not every developer needs the live Stripe key, and most do not want it.
  5. Rotation on a schedule. A key that has been in use for two years has had two years of chances to leak. Payment processor keys and database passwords first.

Most teams I have seen stop at level two and consider it done. Levels three and four are the ones that decide whether a mistake is a rotation or an incident. Rotation itself is a five-minute job when the new value comes from the password generator, gets pasted into the platform settings, and the old one is revoked after the deploy.

* * *

Each framework draws the browser line differently

The one thing that matters in a web framework is which variables reach the browser, because anything that does is public.

  • Next.js: only variables prefixed NEXT_PUBLIC_ are bundled into client code. Everything else is server-only. NEXT_PUBLIC_STRIPE_SECRET_KEY is a variable name that should not exist; if it does, the key is in the JavaScript anyone can download.
  • Vite: same idea, VITE_ prefix, read via import.meta.env and replaced at build time. process.env is not defined in the browser bundle.
  • Node.js: since Node 20.6 the runtime reads a file itself with node --env-file=.env app.js, and Node 22 added process.loadEnvFile(). The dotenv package still works and is no longer required.
  • Python and Django: python-dotenv or django-environ, then os.environ.get("NAME", "default"). Give a default where one is safe.
  • Docker: environment: in docker-compose.yml, -e on the command line, or env_file:. Values set on the container override the file.

The mistakes I see in defect reports: a variable that is undefined in the browser because the prefix is missing; a variable read at build time on a platform that only provides it at runtime, so the deployed value is the placeholder; and a password with an @ in it that breaks the connection string because nobody URL-encoded it. Quote values with special characters, or better, generate passwords without them.

Developer working on configuration files on laptop
Developer working on configuration files on laptop
* * *

The right kind of random for each variable

  • JWT and session secrets: at least 256 bits of randomness, 32 bytes, base64 or hex encoded. A short secret can be brute-forced, and then anyone can mint valid tokens.
  • API keys you issue to your own users: 32 random alphanumeric characters is roughly 190 bits, which is more than enough. Prefix them (tf_live_) so a leaked key is recognisable in a log.
  • Database passwords: 16 characters minimum, and I would avoid the characters that need escaping in a connection string (@, :, /, ;) rather than remember to encode them.
  • Encryption keys: exactly the size the algorithm wants. AES-256 wants 32 bytes. Padding a shorter key to length is not a 256-bit key.
  • Identifiers: v4 UUIDs from the UUID generator for tenants, instances and configuration profiles.

The password generator covers the first three; set the length and the character set and paste. Then run the finished file through the .env.example generator before the first commit, so the committed copy has the names and none of the values.

* * *

FAQ

I committed a .env file. What now?

Treat every value in it as compromised and rotate them all, today. Then remove the file from history with git filter-repo or BFG Repo-Cleaner and force-push. The cleanup is disruptive for everyone with a clone, which is why the .gitignore line in the first commit is worth more than any cleanup tool.

Should production use a secrets manager instead of .env files?

Yes. The hosting platform's own environment settings, AWS Secrets Manager or HashiCorp Vault give you access control, an audit log and rotation. For local development a .env.local file is fine; the threat model is different.

How do I share variables with a colleague?

Not through git and not in chat. A shared vault in a password manager (1Password, Bitwarden) for development credentials, or the team's secrets manager. The .env.example in the repository tells them which variables they need; the vault gives them the values.

Can another process on the same machine read my environment variables?

On most operating systems, any process running as the same user can. It is one more reason production secrets belong in a secrets manager rather than in the process environment of a shared box. On your own laptop it is a risk I would accept.

Key takeaway

### I committed a .env file.