Stop Shipping API Keys in Your Frontend Bundle
How secrets sneak into client JavaScript through public env prefixes, and the build-gate audit that catches them before deploy.
Your secret is not secret if it ships in the JavaScript. This sounds obvious, and yet it is one of the most common ways real credentials end up exposed in production, because the framework makes it a single typo away. A developer needs a value in a React component, reaches for an environment variable, prefixes it the way the docs show for client-side values, and ships an API key into a file that anyone can download by opening the browser's network tab. The key works perfectly. It also works perfectly for whoever finds it.
To stop leaking secrets in your frontend bundle, never put a real credential behind the NEXT_PUBLIC_ prefix, because that prefix inlines the value straight into client JavaScript that anyone can read. Keep secrets in server-only environment variables, reach them only from server code or API routes, and scan each build to confirm no secret was compiled into the bundle.
The frontend bundle is public. Everything in it is readable by anyone who visits your site, because their browser has to download and run it. There is no obfuscation that changes this, no minification that hides it, no build step that protects it. If a secret is in the client JavaScript, it is published. The job is to make sure none of them ever get there, and to verify it on every build rather than trusting that nobody made the typo this time.
How secrets get into the bundle
In Next.js, the mechanism is the NEXT_PUBLIC_ prefix, and it is doing exactly what it is designed to do. Any environment variable prefixed with NEXT_PUBLIC_ is inlined into the client-side JavaScript at build time, baked directly into the bundle so it is available in the browser. That is the intended behavior for genuinely public values: an analytics tracking ID, a public API endpoint, a feature flag that does not gate anything sensitive.
The danger is that the same mechanism does not know the difference between a public tracking ID and a private API key. It inlines whatever you prefix. So when a developer has a secret in an environment variable and needs it in a component, the path of least resistance is to add NEXT_PUBLIC_ to make it available client-side, and the moment they do, the secret is compiled into the bundle and shipped to every visitor. The build does not warn them. The app works. The leak is invisible until someone goes looking, and on the internet, someone always goes looking.
The variations on this are worth naming because they hide in plain sight:
- A real secret given the public prefix because a component needed it and that was the quickest way to get it there.
- An internal service URL exposed client-side, handing an attacker a map of your infrastructure they would otherwise have to discover.
- A feature gate or admin flag inlined into the client, where it can be read and sometimes flipped, because the gating logic lives where the user can see it.
- An env-var fallback in code, a default value hardcoded for when the variable is missing, that ships a real credential as the fallback.
Each of these is a different route to the same outcome: something that should have stayed on the server ends up in a file the user downloads.
The rule: public means public, treat the prefix as a publishing decision
The mental model that prevents this is to read NEXT_PUBLIC_ not as "make this available to the frontend" but as "publish this to the entire internet." Because that is what it does. Once you frame it that way, the question for every public variable becomes "am I comfortable with anyone in the world reading this value," and for a secret the answer is obviously no.
So the rule is simple and absolute. Only public configuration goes in public variables: API endpoints that are meant to be called from the browser anyway, feature flags that do not protect anything, analytics IDs that are harmless if seen. Everything else, every key, token, credential, internal URL, and secret, stays server-side, where it is read only in server code and never crosses into the bundle.
This is the same principle that runs through good security design generally: a secret loaded from the environment, never hardcoded, never given a fallback default, and never exposed to the client, and it pairs with keeping production secrets separated per concern so one leak never compromises the rest, and being able to rotate production secrets without taking the app down the moment one does leak. When the client needs to do something that requires a secret, the client does not get the secret. The client calls a server endpoint, the server uses the secret, and the secret never leaves the server. That extra hop is the difference between a credential that is protected and one that is published, and a scoped API token makes the blast radius small even if that server-side credential does leak.
Audit the bundle on every build, because the typo will happen
Discipline catches most leaks. Verification catches the rest, and the rest is what gets you. The reason to audit rather than trust is that the leak is a single prefix away and produces no error, so it will eventually happen to a tired developer at the end of a sprint. The defense is to check the built bundle for secrets before it deploys, every time, automatically.
After the build, the secrets, if any leaked, are sitting in the generated JavaScript in the static chunks directory. You can confirm this manually: build the app, then search the files in .next/static/chunks for the values you care about, your API keys, your tokens, your internal URLs. If a secret shows up in those files, it is in the bundle and it is exposed. The same search done in the browser's network tab on the deployed site shows you exactly what a visitor can see.
Make this a gate, not a one-time check. Add a step to the quality gates a developer runs before deploying, the same suite that runs the type checker and the linter, that greps the built client chunks for a list of known-secret patterns and fails the build if any are found, the same enforce-it-in-the-gate discipline behind a performance budget your team will not quietly break. A build that ships a secret should not ship at all. This turns "we hope nobody used the wrong prefix" into "the build refuses to deploy if anyone did," which is the only version of this that holds up over time and across a growing team.
# fail the build if any known secret pattern appears in the client bundle
grep -rE "(sk_live|secret|PRIVATE_KEY)" .next/static/chunks && exit 1 || exit 0
The exact patterns depend on what your secrets look like, but the principle is fixed: enumerate the things that must never appear client-side, and fail loudly if they do.
Where this fits in a real security review
Leaked frontend secrets are one of the first things we check in a security audit, because they are common, high-impact, and trivially exploitable. An exposed API key is not a theoretical risk, it is a working credential that an attacker can use immediately, often to run up charges on your account, access data, or pivot deeper into your systems. And the internal URLs and feature gates that leak alongside the obvious keys hand an attacker reconnaissance they would otherwise have to work for.
It pairs with the broader principle that the client is untrusted ground. Anything that must be protected has to be protected on the server, the same reason you pin third-party scripts with subresource integrity instead of trusting whatever the CDN serves. Everything that reaches the client is, by definition, in the hands of the user, friendly or not. We build the web applications we ship with that boundary drawn hard: secrets live server-side, the client gets only what is safe to publish, and the build verifies the line was not crossed, alongside the security headers every Next.js app should ship so the public surface is hardened as well as the secrets. If you want to know whether your own bundle is leaking something it should not, our free security scan is a fast way to start looking.
The short version
The frontend bundle is published to everyone, so treat the public-variable prefix as a decision to publish, not a convenience for reaching the client. Keep every secret, token, internal URL, and feature gate server-side, and when the client needs something a secret unlocks, route it through a server endpoint instead of handing over the secret. Then verify: search your built client chunks for your secrets, and gate the build so it refuses to ship if any are found.
Discipline prevents the leak you are thinking about. The build gate prevents the one you are not, the prefix added in a hurry by someone who just needed the value and did not realize they were publishing it to the world.






