http://undefined/api/login — When Next.js Environment Variables Are Undefined in the Browser
In PHP, $_ENV is available everywhere. In Next.js, the server and browser are separate builds — a variable without the NEXT_PUBLIC_ prefix becomes undefined in the client, and nothing tells you.
POST http://undefined/api/login net::ERR_NAME_NOT_RESOLVED
This was the symptom. An environment variable that was clearly set in .env — API_BASE_DOMAIN=api.shift.test — read as undefined in the browser. The API calls were trying to reach http://undefined/api/login instead of the real domain.
In PHP, this would never happen. $_ENV['API_BASE_DOMAIN'] returns the same value whether you read it in a controller, a template, or a CLI command. Environment variables are a global constant, accessible from any layer.
Next.js does not work this way.
Two builds, two worlds
Next.js produces two separate bundles: one for the server (Node.js) and one for the browser (JavaScript). Environment variables from .env are available to the server bundle by default. Variables prefixed with NEXT_PUBLIC_ get inlined into the browser bundle at build time.
.env
API_BASE_DOMAIN=api.shift.test ← server only (undefined in browser)
NEXT_PUBLIC_API_BASE_DOMAIN=api.shift.test ← available everywhere
The inlining is a compile-time substitution: Next.js literally replaces process.env.NEXT_PUBLIC_API_BASE_DOMAIN with the string value in the client bundle before bundling. There is no runtime lookup in the browser.
The PHP reflex
In a PHP application, you never distinguish between "server-side" and "client-side" environment variables because there is no client-side runtime — everything runs on the server, and the HTML is already rendered by the time it reaches the browser. The concept of "this variable exists but only in one bundle" does not apply.
When a Next.js project shows undefined for a known .env value, the natural reflex is to check the spelling of the variable name, the file path, or the .env syntax. All of those are correct. The problem is the missing NEXT_PUBLIC_ prefix — a distinction that has no equivalent in PHP.
Consequence: change requires a restart
Because NEXT_PUBLIC_ variables are inlined at build time, simply saving a new value in .env and refreshing the browser is not enough. For a running dev server, you must restart it (Ctrl+C → pnpm dev). For a containerised setup, you must rebuild the image.
In PHP, editing .env and refreshing the page picks up the new value (assuming OPcache is configured to revalidate). The immediacy of environment variable changes is something you rely on without noticing — until it stops working.
Rule of thumb
Any variable read in a "use client" component, a hook, or a utility imported by a client component must have the NEXT_PUBLIC_ prefix. Server Components, Route Handlers, and Server Actions do not need it.
When you see an http://undefined/... URL in your browser, your first check should not be the .env file — it should be the NEXT_PUBLIC_ prefix.
Delaa