guide6 min readupdated 2026-04-17

What is NEXT_PUBLIC_ in Next.js env variables?

The NEXT_PUBLIC_ prefix decides whether an env variable is shipped to the browser or kept on the server. Here's how it works, when to use it, and the traps.

TL;DR

In Next.js, any environment variable whose name starts with NEXT_PUBLIC_ is inlined into the client bundle at build time. Everything else stays server-only. Get this wrong and you either (a) leak secrets to the browser or (b) read undefined in your React component and wonder why.

Server-only by default

Next.js treats process.env.ANYTHING as server-only unless the key starts with NEXT_PUBLIC_. This is a deliberate security default — it means DATABASE_URL, STRIPE_SECRET_KEY, and OPENAI_API_KEY can never accidentally ship to the browser just because you imported them in a shared module.

In practice:

# .env.local
DATABASE_URL=postgres://...               # server only
STRIPE_SECRET_KEY=sk_test_...             # server only
NEXT_PUBLIC_SITE_URL=https://myapp.com    # browser + server
NEXT_PUBLIC_STRIPE_PK=pk_test_...         # browser + server

How the build pipeline handles it

When you run next build, Next.js walks every client module and does a textual replacement — every reference to process.env.NEXT_PUBLIC_FOO becomes the literal string of that value, baked into the JavaScript bundle. Two consequences:

  1. Changing a NEXT_PUBLIC_ value requires a rebuild. The production bundle is frozen. Editing the env in Vercel's dashboard won't update the running app until you redeploy.
  2. Non-public variables aren't bundled. A client component referencing process.env.DATABASE_URL will just get undefined at runtime. Next.js doesn't warn — it silently resolves to undefined.

What should be public?

Keys that are designed to be in the browser:

  • Public Stripe publishable key (pk_*)
  • Supabase URL + anon key (RLS protects data)
  • Firebase client config
  • Analytics / tracking IDs (Mixpanel, PostHog, Segment)
  • The site's own URL for generating absolute links

What should NEVER be public

  • Database URLs (credentials are in the string)
  • Stripe secret key (sk_*)
  • Supabase service-role key
  • Firebase Admin SDK credentials
  • OpenAI / Anthropic API keys
  • OAuth client secrets
  • JWT signing keys

If you're unsure, run your .env through the leak checker — it flags every key that matches a known secret pattern. If any of those have NEXT_PUBLIC_ prefixes, fix immediately.

Common mistakes

Using the key inside a client component without the prefix

"use client";
// ❌ undefined in the browser — silent failure
const url = process.env.API_URL;

// ✅ renamed with prefix — works
const url = process.env.NEXT_PUBLIC_API_URL;

Prefixing a secret by accident

# ❌ ships STRIPE_SECRET_KEY to every browser
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_...

Yes, this happens. Code review for it. The leak checker catches sk_live_* patterns specifically.

Changing the value without rebuilding

You updated NEXT_PUBLIC_API_URL in the Vercel dashboard. Nothing changes. Because the value is baked into the bundle, you need to redeploy (even a no-op deploy works).

Forgetting server vs client semantics in Next.js App Router

In the App Router, "use client" components get the client bundle rules. Server components get the full env. Mixing them in one file is where most bugs come from — keep env access in server components and pass values down as props.

A safe default pattern

// lib/env.ts — server
import { z } from 'zod';
const schema = z.object({
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
});
export const env = schema.parse(process.env);

// lib/env.client.ts — client
const schema = z.object({
  NEXT_PUBLIC_SITE_URL: z.string().url(),
  NEXT_PUBLIC_STRIPE_PK: z.string().startsWith('pk_'),
});
export const env = schema.parse(process.env);

Two schemas, two imports, no way to accidentally ship a server key to the browser.

FAQ

Why is my NEXT_PUBLIC_ variable still undefined?

Next.js inlines NEXT_PUBLIC_ variables at build time, not at runtime. If you added it after starting the dev server, restart. If you added it after deploying, you need to rebuild — changing the value in the host's dashboard won't update an existing build.

Can I expose NEXT_PUBLIC_SUPABASE_ANON_KEY safely?

Yes. The anon key is designed to be public — Row Level Security protects your data. The service-role key is the one you must keep server-only. Same pattern for Firebase, Stripe publishable keys, Mixpanel tokens.

Does NEXT_PUBLIC_ work in server components?

Yes, it works everywhere. It just also works in the browser. Non-prefixed variables only exist on the server.

Related reading

Related tools

Continue reading