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.