All posts

Field guide

NEXT_PUBLIC: what it does and when it leaks your secrets

The NEXT_PUBLIC prefix is one of the most misunderstood things in a Next.js app. It does exactly one job, publish a value to the browser, and using it on the wrong variable ships your secret to every visitor. Here is the rule, and the safe pattern.

Last updated

If you build with Next.js, you have seen the NEXT_PUBLIC_ prefix, and it is easy to misread it as some kind of scoping or safety marker. It is not. It is a publish instruction, and understanding that one fact is the difference between a working config and a leaked secret.

What the prefix does

At build time, Next.js takes every environment variable that starts with NEXT_PUBLIC_ and inlines its actual value into the client JavaScript bundle, so the browser can read it. Variables without the prefix are never sent to the browser; they stay on the server.

# shipped to the browser — value is in the bundle every visitor downloads
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...   # OK: meant to be public
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_...        # LEAK: this is a secret

# server-only — never sent to the browser
STRIPE_SECRET_KEY=sk_live_...                    # correct place for a secret

So the prefix is doing its job perfectly in both cases; it publishes what you tell it to. The mistake is telling it to publish a secret.

Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.

See a live scan →

The rule

Prefix a variable with NEXT_PUBLIC_ only if you are comfortable with the whole world reading its value. Publishable keys, a Supabase anon key, a public API base URL: fine. Anything that grants privileged access, a secret key, a service-role key, a token, a database URL: never. There is no middle ground, because the bundle is public by definition.

The safe pattern for a secret

Keep the secret in a non-prefixed variable and use it only on the server. The browser calls your server, and the server uses the key:

// app/api/charge/route.ts — runs on the server only
export async function POST(req: Request) {
  const key = process.env.STRIPE_SECRET_KEY;  // no NEXT_PUBLIC prefix
  const result = await callStripe(key, await req.json());
  return Response.json(result);               // browser gets the result, not the key
}

The key never enters the bundle, so there is nothing to leak. This is the same principle as why an environment variable alone doesn't keep a secret out of the browser, and the broader idea of client-side vs server-side secrets.

Frequently asked questions

What does the NEXT_PUBLIC prefix actually do?

It tells Next.js to expose that environment variable to the browser by inlining its value into the client JavaScript bundle at build time. Any variable without the prefix stays server-only. The prefix exists so you can deliberately ship public values, like a publishable key or a public API base URL, to the client. It is a publish instruction, not a security setting.

Is it safe to put my API key in a NEXT_PUBLIC variable?

Only if the key is meant to be public, like a Stripe publishable key or a Supabase anon key. For a secret key, one that grants privileged access, no. Prefixing it with NEXT_PUBLIC bakes the real value into the bundle every visitor downloads, so it is fully exposed. A secret key must stay in a non-prefixed variable, used only in server code.

How do I use a secret key in Next.js then?

Keep it in a plain environment variable with no NEXT_PUBLIC prefix, and reference it only in server-side code: a route handler, a server action, or getServerSideProps. When the browser needs something that requires the key, it calls your server, the server uses the key, and only the result is returned. The key value never enters the client bundle.

I removed the prefix but the key still shows in an old build. Why?

Because NEXT_PUBLIC values are baked in at build time, so a bundle built while the prefix was present still contains the value. Rebuild and redeploy after removing the prefix. And since the value was public in that earlier build, treat it as compromised and rotate the key, not just remove the prefix.

Check what your bundle exposes

The tricky part is that a NEXT_PUBLIC secret looks fine in your editor and only appears in the built bundle. A scan reads what your deployed app actually serves and flags a secret key that made it into the browser. Run a free scan and see what your NEXT_PUBLIC variables are shipping.

Prbl scans your live app or your codebase for exactly the kinds of issues above.

NEXT_PUBLIC: What It Does and When It Leaks Your Secrets: Prbl