Environment variables done right (and where they leak)
Right idea, easy to undo
Environment variables exist so you do not hardcode secrets into your source. You put DATABASE_URL or STRIPE_SECRET_KEY into the environment, read it at runtime, and the value never lives in a committed file. That part is solid and you should absolutely do it.
The trouble is the second half: in modern frontend tooling, some environment variables are deliberately shipped to the browser. If a secret lands in that group, the env var did not protect it at all — it just gave you a false sense that it did.
The prefix that changes everything
Frontend frameworks need a way to pass public config into client code — an analytics ID, a publishable key, an API base URL. They do this with a naming convention: a variable becomes browser-visible if it starts with a specific prefix. Common ones:
NEXT_PUBLIC_in Next.jsVITE_in Vite-based appsPUBLIC_in some setupsREACT_APP_in older Create React App projects
At build time, every variable with that prefix is baked into the JavaScript bundle as a plain string. That is by design — but it means the prefix is a publish switch. Put NEXT_PUBLIC_STRIPE_SECRET_KEY in your env and you have shipped your secret key to every visitor, prefix and all.
The simple rule
- Public prefix = public value, always. Only put non-sensitive things behind a public prefix: analytics IDs, publishable keys, public URLs. Never a secret.
- Secrets stay unprefixed and server-side. A real secret should only ever be read in server code — an API route, a backend function — never in a component that ships to the browser.
- When in doubt, assume the browser sees it. If you cannot say for certain a variable stays server-side, treat it as public and do not put a secret there.
Where env vars still leak
Even with the prefix understood, a few classic gaps remain:
- The
.envfile gets committed. It belongs in.gitignore. Committed once, it lives in git history even after deletion. .envdeployed into a public folder. If the file ends up served as a static asset, it is one predictable URL away —/.envis a path bots try constantly.- Server var read in client code. Referencing an unprefixed secret from a component can, depending on the bundler, either fail or quietly inline it. Keep the boundary clean.
- Secrets printed into logs during a build or error, where they end up in CI output.
A quick mental check
Before you ship, ask one question per variable: would I be comfortable pasting this value into a public tweet? If yes, a public prefix is fine. If not, it stays server-side, unprefixed, and out of any file the browser or a repo can reach.
Check what actually shipped
The hard part is that a secret baked into a bundle is invisible once it is minified. An automated scan reads your shipped pages and scripts the way a bot would and flags secrets and keys that are reachable from the browser. Scan your site and confirm your env vars stayed where you put them.