Your app ran clean on your laptop for a week. You push it live, and the exact same code, the same files you tested an hour ago, throws an error the moment a real visitor loads the page. Nothing in the code changed between those two runs. What changed is the environment: a value your app expected to find at startup was sitting in a file on your machine, and nobody carried it over to production.

That value is an environment variable: a setting your app reads at runtime instead of hard-coding into a file, so the exact same code can run against two different configurations without you touching a line of source. Change the value and you’ve changed which database it talks to, which API key it authenticates with, or which mode it runs in. That’s the whole idea, and the detail every generic definition skips is this: which environment currently has the value matters more than what the value is.

This post covers both halves: what a .env file actually holds and why your builder gitignored it, and the part that trips up almost every AI-built app, the same variable set once, on a laptop, and nowhere else.

An environment variable is a value your runtime supplies, not a hard-coded constant

An environment variable is a named value your app reads from its runtime, not from a hard-coded constant, and that’s what lets one codebase behave correctly in more than one place. Your local machine, your CI runner, and your production server are three different environments, each capable of handing your app a different value for the same variable name.

Most builders never write the reading code themselves. Lovable, Bolt, Cursor, and the rest generate a line like process.env.DATABASE_URL and move on, because that line is correct by itself. The part they don’t generate is making sure a real value sits behind it everywhere the code runs, for the same reason AI coding tools ship security holes by default: the tool wires the happy path and treats everything downstream, a key existing in a second environment included, as someone else’s job.

The .env file: what’s in it, and why it’s gitignored

Locally, that value lives in a plain text file named .env, sitting in your project root, never committed to git. It’s just key-value pairs:

DATABASE_URL=postgres://user:pass@localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_51Hxxxxxxxxxxxxxxxxxxxxxxxxx
NODE_ENV=development

Your app reads each one the same way:

const url = process.env.DATABASE_URL;

No import, no config file to edit, just a global object your runtime populates from whatever .env (or your shell, or your host) handed it before the process started. Nothing more happens underneath it. The file is gitignored for a specific reason: it holds real credentials, a live database password, a Stripe key, and a repo is a thing other people clone, fork, and search. .gitignore has one job here: the file, and every credential in it, never leaves your machine through git.

That’s the trap: a file that deliberately never leaves your machine is a file whose contents never reach anywhere else either, unless you separately, manually, put them there.

Environment variables in production: why the same app behaves differently

Here’s the belief every vibe coder starts with: the code is identical, so the behavior should be too. .env is local by design and by .gitignore. Your hosting provider (Vercel, Railway, Render, a plain VPS) never sees that file, because it was never supposed to. It builds and runs your code from what git actually pushed, and .env wasn’t in that push.

The code is identical in both places. The config is what changes, and the variable you set once, on your laptop, is the one production never got.

So the deploy succeeds. The build finishes. The server starts. And the first request that touches process.env.STRIPE_SECRET_KEY gets undefined, because the value was never anywhere but a file you never copied over. Depending on what that variable gates, the failure ranges from an obvious crash to something worse: an integration that quietly does nothing, or a feature that just never fires and never tells anyone why. The Deployment & Operations pillar across the 21 third-party apps in the AxonBuild audit corpus, audited in June and July 2026, averaged 37.0 out of 100, the third-worst of twelve, and the recurring shape underneath that number is the same one: a deploy pipeline nobody checked before a customer did, the same gap that turns one database with no staging into a deploy set to break.

Setting environment variables: .env file vs. hosting dashboard

Locally, you edit .env and restart your dev server. In production, there is no file to edit, because there’s no .env file there at all. Instead, every serious host gives you a settings screen, a secrets manager, or a CLI command where you type the same key and the same value, and it injects that pair into the process’s environment before your code ever runs.

What lives in an env var
What does NOT
A database connection string
Business logic (pricing rules, permission checks)
An API base URL or feature flag that differs by environment
Anything the code needs to be deterministic to run correctly
A secret key (Stripe, a signing secret, an AI provider key)
Anything you’d actually want reviewed in version control
What lives in an env var
A database connection string
An API base URL or feature flag that differs by environment
A secret key (Stripe, a signing secret, an AI provider key)
What does NOT
A database connection string
Business logic (pricing rules, permission checks)
An API base URL or feature flag that differs by environment
Anything the code needs to be deterministic to run correctly
A secret key (Stripe, a signing secret, an AI provider key)
Anything you’d actually want reviewed in version control

What’s easy to forget is the second half of this: setting a variable in .env is a different action from setting it on the host. Add a new key locally, ship it, and the feature breaks in production until you separately add the same key on the host’s dashboard. Two places, two steps, and the first gives you no feedback that the second is still outstanding. On a host that builds your site into static files, the dashboard is only the start of the story, which is where environment variables end up on a build-time host like Netlify.

What belongs in an env var, and what a fallback should never quietly do

Most of the time, this works out fine. Secrets & Credentials was the highest-scoring of AxonBuild’s twelve readiness pillars, averaging 84.4 out of 100 across the same 21 third-party apps: an unusual result against the leaked-key stereotype. The typical vibe-coded app gets the storage half right, keys in environment variables instead of hard-coded in a file that ships to the browser. Storing the value in the host’s dashboard is not what keeps it private either, which is part of whether Vercel itself is safe to deploy on.

Using env vars this way is fine practice. What matters is what the code does the one time the variable isn’t there. In one health-records API I audited, the code signing a user’s login cookie was supposed to read a session secret from the environment. That variable was unset in every deploy configuration I found, and the app didn’t fail to start over it: it fell back to a fixed phrase printed directly in the source and kept issuing cookies anyway. Nothing crashed, no startup guard refused to boot over the gap, and “configured” looked identical to “not configured” from every place you’d normally check.

That’s the config-hygiene lesson underneath the secrets stereotype: a required, security-relevant variable should fail loudly when it’s missing, not quietly downgrade to something weaker. A generic feature flag defaulting to false is a reasonable fallback. A signing secret defaulting to a string anyone can read in your own source just leaves the app pretending it’s still configured.

// A required, security-relevant var should refuse to boot, not downgrade:
const sessionSecret = process.env.SESSION_SECRET;
if (!sessionSecret) {
  throw new Error('SESSION_SECRET is not set. Refusing to start on a guessable default.');
}

A production config contract, from a real app

There’s a second, correct way to handle a missing variable: for integrations that are genuinely optional (an email provider, an analytics ID, a CRM key), the right move on a missing key isn’t crashing the whole app over one feature.

This site runs on exactly that contract. Every server-side integration key it reads goes through one small reader function first:

// getSecret comes from astro:env's server module; process.env is the fallback.
export async function readServerEnv(key) {
  const getSecret = await loadGetSecret();
  return getSecret?.(key) || process.env[key] || undefined;
}

It checks the framework’s own secret store first, falls back to the plain process environment, and never throws. If an email API key is missing, the send gets logged and skipped instead of crashing a page over a feature that was never load-bearing. That only works because these variables are the second category above: real, but non-critical. Treat every missing variable that way, the session-secret example included, and you’ve reproduced the exact bug the last section described. Knowing which bucket a variable is in is most of this discipline; the rest is remembering to set it twice. It’s one piece of whether an AI-built app is actually ready to launch, a question a working demo never answers on its own.

Common questions about environment variables

What are environment variables, and why do I even need them?

Because the alternative is hard-coding a value that has to differ by place, your local database versus your production one, a test API key versus a live one, and editing the source every time you switch. An environment variable lets one file run correctly against either, based on what the environment actually provides.

What’s a .env file?

A plain text file of key=value pairs, in your project root, that your local tooling loads into process.env before your app starts. It’s never committed to git, which is why the values inside it never automatically show up anywhere your code gets deployed to.

Why does my app work locally but break in production?

Most often because a variable your .env file provides locally was never separately added to your host’s environment settings. That’s the single most common cause of the two diverging, though not the only one: build steps and platform-specific defaults can differ too.

What should never go in an env var?

Anything the app needs in order to be deterministic, pricing logic, a permission check, code you’d want reviewed and version-controlled, doesn’t belong here. And a security-relevant secret should never carry a fallback quiet enough that the app can’t tell you one is missing.