Building Apps with AILesson 45 of 87

Contents

Building Apps with AI

Lesson 45 of 87. Sign in to keep your place.

Module 5 · Save points, and shipping to a stranger

Environment variables, and the prefix that leaks

9 min read

The prefix is an instruction to the buildNo prefix: server onlyDATABASE_URL, SESSION_SECRET, LLM_API_KEY, theOAuth client secretRead by code that runs on your machine or yourhostNever appears in anything a visitor downloadsBrowser code that needs it calls your ownroute insteadNEXT_PUBLIC_, VITE_, PUBLIC_Copied into the JavaScript bundle when thebuild runsA literal string in a file every visitordownloadsHonest for a public analytics id or a map tilekeyChanging it on the dashboard does nothinguntil you redeployA model asked to call an API from the page reaches for the prefix, because it is the quickest way tomake a browser-side call work. Providers bill the key’s owner, not the person using it.
A model asked to call an API from the page reaches for the prefix, because it is the quickest way to make a browser-side call work. Providers bill the key's owner, not the person using it.

Figure

A string handed to a process

An environment variable is a named string given to a program when it starts. process.env.DATABASE_URL reads one. The value is not in the code and not in git, which is the entire point: the same code runs on your laptop with one database and on the host with another, and neither address is written down anywhere a stranger can read.

Locally, a .env file is a convenience: the dev server reads it and sets the variables for you. On the host there is no .env; there is a settings page where you type each name and value. The earlier lesson said this once; this one is about the ways it goes wrong after you have done that.

Two kinds, and the prefix

Some variables are read only on the server. Some are meant for the browser: the address of your own API, a public analytics id, a map tile key that is designed to be public.

Frameworks mark the second kind with a prefix: NEXT_PUBLIC_ in Next.js, VITE_ in Vite, REACT_APP_ in older React setups, PUBLIC_ in Astro and SvelteKit. The prefix is an instruction to the build: copy this value into the JavaScript bundle. After the build, the value is a literal string inside a file every visitor downloads. Open the site, view source, search for the value, and there it is.

A model asked to “call the weather API from the page” will reach for the prefix, because it is the quickest way to make a browser-side call work. That is a leak. A key with a public prefix belongs to whoever opens the developer tools, and providers bill the key’s owner, not the person using it.

The rule is short. Secrets never get the prefix. Browser code that needs a secret calls your own server route, and the server makes the call. The block on security returns to this with a test you can run on your own bundle.

Build time versus run time

Public variables are inlined when the build runs. Change one on the host’s settings page and nothing happens until you redeploy, because the old value is already baked into the files being served. This is the mechanism behind “I changed it and it still shows the old value”.

Server-side variables vary by host. Some read them fresh at each request; some capture them at build. The symptom is the same and the cure is the same: after changing any variable, trigger a deploy. Most hosts have a “redeploy” button that rebuilds the current commit for exactly this.

undefined is a word

When a variable is missing, process.env.API_URL is undefined, and JavaScript will happily put that into a string:

JavaScript
fetch(`${process.env.API_URL}/bookings`)   // fetches "undefined/bookings"

The request goes to a path that does not exist and the error mentions a 404, not a missing variable. Hours have gone into this. The defence is to fail loudly at startup, in one small file:

TypeScript
const required = ["DATABASE_URL", "SESSION_SECRET"];
for (const name of required) {
  if (!process.env[name]) throw new Error(`Missing environment variable: ${name}`);
}

Now a missing variable stops the app on its first request with a message that names it. Libraries like zod let you validate shape as well as presence (that the port is a number, that the URL starts with postgres://) and the same idea applies.

Three environments

Local, preview and production should hold different values, and the one that matters most is the database. A preview deployment of an unmerged branch pointed at the production database is how a migration test wipes real users’ data. Hosts let you scope each variable to an environment; use it, and give previews a database of their own or a branch of the real one (Neon and similar services create database branches for exactly this).

Where the local file goes wrong

Next.js reads .env, then .env.local, and later files override earlier ones. A stale value in .env.local beats the fresh one you just put in .env. A variable exported in your shell profile beats both. When a value refuses to change, ask the process what it actually sees:

Bash
node -e "console.log(process.env.DATABASE_URL)"

And keep .env.example committed with every name and a fake value. It is the map that makes a new machine or a new collaborator take five minutes, and it is the only place the full list is written down.

Every lesson here is open. Your progress is recorded once you pass the Building with AI exam.

Check your understanding

An app calls a weather service directly from the browser using NEXT_PUBLIC_WEATHER_KEY, and it works. What is wrong?

Pick the one you would defend. Your answer stays private.

Next lesson · 8 minWhich version is live

Stuck on something?Ask Beatriz, your AI tutor

How to cite: Addaly (2026) ‘Environment variables, and the prefix that leaks’, in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/environment-variables-and-the-two-worlds (Accessed: 13 October 2026).