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.gitignorein 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.envin 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_URLis ambiguous when there are two databases.DATABASE_URLandANALYTICS_DATABASE_URLare 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.

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.
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.
- Secrets out of code. The baseline. Whoever sees the source (open source, a leaked repository, a shared screen) does not see the keys.
.envout of git. The.gitignoreline, and once in a whilegit log --all --full-history -- '.env'to confirm nothing ever slipped in. If it did, rotate every value in that file; history is permanent.- Different secrets per environment. The development database password is not the production one. A stolen laptop should not be a production incident.
- 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.
- 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_KEYis 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 viaimport.meta.envand replaced at build time.process.envis 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 addedprocess.loadEnvFile(). Thedotenvpackage still works and is no longer required. - Python and Django:
python-dotenvordjango-environ, thenos.environ.get("NAME", "default"). Give a default where one is safe. - Docker:
environment:indocker-compose.yml,-eon the command line, orenv_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.

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.
### I committed a .env file.
Email Validation: What a Validator Can Check, What Only a Confirmation Email Proves, and the Bounce Rate That Gets You Blocked
Syntax, typo and disposable checks catch the easy mistakes; only a confirmation email proves the address is real. How a bounce rate above two percent damages your sending reputation, the form rules I test for, and how to clean a list you inherited.
JSONPath: Query Nested JSON Without Counting Braces
JSONPath syntax as standardised in RFC 9535, how I find a field in a 2,000-line API response, jq one-liners, three ways to flatten to CSV, and the six JSON mistakes that cost the most time.
How to Generate Fake Test Data for Development
How to generate realistic fake test data for development and prototyping. Covers names, emails, UUIDs, and addresses with free browser tools.
Best Free Online Developer Tools in 2026
The best free online developer tools for 2026: JSON formatters, regex testers, API builders, and code converters. All browser-based, no install.
