Bugsy
Explains
All explainers

Bugsy explains

Environment variables and .env

How one build talks to localhost on your laptop and to the real API in production, which source wins when a variable is set twice, and why secrets never get the NEXT_PUBLIC_ prefix.

7 min 1 toy

  • devops
  • tooling
  • security

On your laptop, your app calls an API at http://localhost:4000. In production, the very same code calls https://api.example.com. Nobody edits the code between the two. So where does the code find out which address to use?

From its environment: a small set of settings handed to the program when it starts, from outside the code.

What an environment variable is

Every running program gets a list of NAME=value pairs from whatever started it. That list is its environment. Your terminal's shell has one, and every program you start from it gets a copy.

Node.js shows the list as process.env. Reading process.env.API_URL gives the value as a string, or undefined when nothing set it.

export API_URL=https://staging.example.comnode -e "console.log(process.env.API_URL)"

export adds API_URL to the shell's environment, so the node started on the next line inherits it and prints https://staging.example.com. Writing API_URL=https://staging.example.com npm run dev sets it for that one command only.

Why config lives outside the code

Config is everything that changes between places the app runs: API addresses, database passwords, API keys. Code is what stays the same. The twelve-factor app guidelines sum it up: keep config in the environment, not in the code.

Then one build can run on your laptop, a test server and production, each with its own settings. And passwords never end up in the code. A good test: could you publish your code today without leaking a single credential?

.env files, and who wins

Typing export for every variable gets old, so tools read them from a file called .env, one NAME=value per line. Next.js loads these files for you. Plain Node.js can do it with node --env-file=.env (Node 20.6 and later) or the dotenv package.

When the same name is set in several places, Next.js checks them in a fixed order and the first one found wins. In development, strongest first: the real environment (your shell, or your host's settings) > `.env.development.local` > `.env.local` > `.env.development` > `.env`. A file never overwrites a variable that is already set, and dotenv behaves the same way by default.

A production build reads .env.production.local and .env.production in place of the development files. The pattern is the same: a .local file beats its shared twin, and the real environment beats every file. One exception: .env.local is skipped while tests run (NODE_ENV is test), so tests get the same values on every machine.

Server code and browser code

Server code, like API routes and server components, runs on a machine you control, so it can read every variable. Browser code runs on your visitors' devices, and Next.js only gives it variables whose names start with NEXT_PUBLIC_.

To do that, Next.js swaps process.env.NEXT_PUBLIC_API_URL in the browser's JavaScript for the value itself, at build time. So the value is public: anyone can open the downloaded JavaScript and read it. And changing it later takes a new build.

Try it: switch sources on and off to see who wins, turn them all off, then add the prefix.

Try it

Quick check

Commit the example, ignore the real thing

.env.local holds your real values, and some are secrets. Anything committed to git stays in its history, even after you delete the file, and everyone with access to the repository can read it. So env files go in .gitignore, the list of files git never tracks.

Instead, commit a .env.example: the same names with placeholder values, like API_URL=http://localhost:4000 or STRIPE_SECRET_KEY=. A new teammate copies it to .env.local and fills it in. It documents what the app needs without leaking anything.

Quick check

Boss question

Put it all together.

Quick check

Done reading?

Solve 3 more quick checks to finish.