The Agent Ledger

← All fixes

Fix: a setting shows as "undefined" in the browser of your AI-built Next.js app (environment variable)

By The Agent Ledger. Drafted with AI assistance and reviewed by the publisher. Tested 2026-10-24.

Status: Reproduced and fixed on a synthetic app, checked in a headless browser. We reproduced the problem and verified the fix on 2026-10-24 with a tiny invented Next.js app on one Linux machine (Node 20.19.2, next 16.4.3, react 19.3.1), using headless Google Chrome 155.0.8059.84 driven by playwright-core 1.64.1. Before the fix the page showed 'undefined/book'; after renaming the variable and rebuilding it showed the real value, in both production and dev mode. We also verified that adding the variable after the build does nothing until you rebuild. We did NOT test other Next.js versions, the Pages Router, Vercel/Netlify or other hosts, Vite-style apps, or code generated by Cursor, Lovable, Bolt or v0. No real systems, keys or private data were used.

Short answer: If a setting such as your booking link or API address works on the server but shows up as undefined in the browser, either the variable's name doesn't start with NEXT_PUBLIC_, or you added it after the app was built. The smallest fix is to rename it (and the line of code that reads it) with the NEXT_PUBLIC_ prefix, then rebuild. Verified on a synthetic app only.

Prerequisites

The exact symptom

There is no red error. This is what we saw in headless Chrome after clicking the button (script: pw/check.js):

text shown after clicking "Book a call": "undefined/book"

The page's code was process.env.BOOKING_URL + "/book", and BOOKING_URL=https://booking.example.test was set in .env.local. The build log even says Environments: .env.local and exits 0. If your code sends that value to a service, you will typically see failed requests or undefined inside a URL in your browser's Network tab instead (we did not test that part).

Versions we used

Item Version
Node.js 20.19.2
next 16.4.3 (Turbopack)
react / react-dom 19.3.1
Browser Google Chrome 155.0.8059.84, headless, via playwright-core 1.64.1
OS Linux, single machine

Cause

Next.js runs your code in two places: on the server, and in your visitor's browser. The browser can't see your server's settings. To let it see a setting, Next.js copies the value into the downloaded JavaScript when you run next build, but only for variables whose names start with NEXT_PUBLIC_, and only if the variable exists at that moment (official docs).

That gives two common ways to get undefined:

  1. No prefix. BOOKING_URL stays on the server. In the browser, process.env.BOOKING_URL is empty. In our built bundle the code was still a lookup at click time: process.env.BOOKING_URL+"/book" (bundle-grep.log).
  2. Added too late. NEXT_PUBLIC_BOOKING_URL is right, but it wasn't set when next build ran (for example you added it to your host's settings after the last deploy). The value is "frozen" at build time, so the browser still gets undefined until you rebuild. We reproduced this too: building with no variable, then adding .env.local and restarting next start still gave undefined.

Public reports of the same family: the Next.js discussion #74624 (Next 15 + React 19 + Turbopack) and a Stack Overflow answer saying env files are read when the server starts and production needs a rebuild. We found these through web search and did not measure how common the problem is, or how often AI tools cause it.

Smallest working fix

  1. Rename the variable everywhere it is set (.env.local and your host's settings) from BOOKING_URL to NEXT_PUBLIC_BOOKING_URL.
  2. Change the code that reads it to the same name, written exactly as process.env.NEXT_PUBLIC_BOOKING_URL.
  3. Rebuild and redeploy (locally: stop the server, run npx next build, start again; in dev, stop and restart npm run dev). On a host, trigger a new deploy after saving the variable.

Before (shows undefined/book): app/BookButton.js

"use client";
import { useState } from "react";

export default function BookButton() {
  const [link, setLink] = useState("");
  return (
    <div>
      <button onClick={() => setLink(process.env.BOOKING_URL + "/book")}>Book a call</button>
      <p id="link">{link}</p>
    </div>
  );
}

.env.local: BOOKING_URL=https://booking.example.test

After (shows the real link)

-      <button onClick={() => setLink(process.env.BOOKING_URL + "/book")}>Book a call</button>
+      <button onClick={() => setLink(process.env.NEXT_PUBLIC_BOOKING_URL + "/book")}>Book a call</button>

.env.local: NEXT_PUBLIC_BOOKING_URL=https://booking.example.test

Write it exactly as process.env.NEXT_PUBLIC_NAME. Per the Next.js docs and the discussion linked above, forms like process.env[name], saving process.env in a variable first, or optional chaining (process?.env?.NAME) are not replaced. We did not test those forms.

If the value must be changeable without rebuilding, or must stay secret, don't use NEXT_PUBLIC_. Read the value in server code and give the browser only what it needs, for example through your own API route. We did not test that approach.

Copyable prompt for your AI coding tool

In my Next.js App Router app, a setting works on the server but is undefined in the browser. Find every place in files marked "use client" (or components imported by them) that reads process.env.SOMETHING where SOMETHING does not start with NEXT_PUBLIC_. For each one, tell me whether the value is safe for any visitor to see. For the safe ones only, rename the variable to NEXT_PUBLIC_SOMETHING in the code, in .env.local and in .env.example, and write the access exactly as process.env.NEXT_PUBLIC_SOMETHING (no optional chaining, no process.env[name], no aliasing). For secrets (keys, tokens, passwords), do NOT add the prefix; instead move that code to a server route and tell me. Do not change anything else, show me the diff, and remind me to set the renamed variables on my host and redeploy.

Review the diff yourself and check no secret got the prefix. We did not test this prompt in any AI tool.

How we verified

Run on 2026-10-24 in fix-04/before, fix-04/after and fix-04/late, using headless Chrome with default settings (script: pw/check.js, which opens the page, clicks "Book a call" and prints the text shown).

  1. Builds pass either way: next build exited 0 for before and after (before-build.log, after-build.log). The build does not catch this.
  2. Before, production (next build + next start): after the click the page showed undefined/book (before-prod-browser.log).
  3. Before, dev (next dev): same result, undefined/book (before-dev-browser.log).
  4. After, production and dev: the page showed https://booking.example.test/book (after-prod-browser.log, after-dev-browser.log).
  5. Bundle check: the built "before" code still reads process.env.BOOKING_URL at click time, while the "after" code has the link written into it (bundle-grep.log).
  6. Added-too-late case (fix-04/late, correctly named variable):
  7. built with no .env.local, then started: undefined/book (late-1-prod-browser.log);
  8. then added .env.local and restarted the server without rebuilding: still undefined/book (late-2-prod-browser.log);
  9. then rebuilt and restarted: https://booking.example.test/book (late-3-prod-browser.log).

Every run also logged one unrelated 404 resource error that we did not identify (probably a missing icon file); we did not confirm which file it was.

Files: see the downloads below.

Not verified

Troubleshooting

Security notes

Downloads

Synthetic example files from our test. No real keys or data.

Get The Agent Ledger every Tuesday

Free. A practical AI workflow or agent demonstration each week, in plain language, with the setup steps, what it costs and what stays manual.

Free to subscribe. One email a week, on Tuesdays. Unsubscribe any time. We use your email only to send the newsletter. Privacy