Anything shipped to the browser, NEXT_PUBLIC_ variables, hardcoded keys, source maps, a committed .env file, is readable by anyone who opens devtools or views your page source. There is no client-side hiding place. The fix is to find what is already exposed, move real secrets server-side, and rotate the keys that leaked.
Why frontend keys are public by design
Anything that runs in the browser is downloadable. Your JS bundle, your .env-derived build output, your inline script tags, all of it is just text a visitor's browser fetches and executes. There is no sandbox that keeps a string secret once it ships to the client, no matter how it is obfuscated or minified.
This is not a bug you can code around. It is why the entire model of "public" versus "secret" keys exists: some keys are designed to be seen (an anon key, a publishable key), and some are designed to never leave your server. The mistake is treating a secret key as if shipping it to the frontend were a configuration detail instead of a leak.
Safe vs unsafe keys
Most platforms give you two classes of key, and mixing them up is the single most common leak LeakRank sees.
Safe to expose (public, publishable, anon):
- Stripe's
pk_publishable key - Supabase's
anonkey - Firebase's
apiKey(an identifier, not a secret) - Any key explicitly documented as "safe for client-side use"
Never expose (secret, private, service-role):
- Stripe's
sk_secret key - Supabase's
service_rolekey - Any third-party API key without a documented public/private split (assume it is private by default)
The prefix or naming convention exists precisely so you can tell them apart at a glance. When a platform does not clearly label a key, treat it as secret until proven otherwise.
How to find leaked keys
Browser devtools. Open your live site, open devtools, and check the Sources and Network tabs. Search the loaded JS for key, secret, token, and any provider prefixes you use (sk_, service_role, etc.). If it is visible here, it is visible to everyone.
Search the JS bundle directly. Fetch your built bundle and grep it:
curl -s https://yourapp.com/_next/static/chunks/*.js | grep -oE '(sk_live_|service_role)[A-Za-z0-9._-]+'
This catches keys that are technically minified but not actually hidden, minification changes variable names, not string contents.
Check source maps in production. If source maps are deployed alongside your build, anyone can reconstruct readable source from the minified bundle, including any secret that was inlined at build time rather than fetched at request time. Confirm .map files are not publicly reachable in production.
Check .env in the repo. Run git log --all -- .env and grep your full history, not just the current tree, for key-shaped strings. A .env committed once and later deleted is still in git history and still exposed to anyone with repo access, including a public GitHub repo indexed by scrapers.
Move secrets server-side
The structural fix is to never let a secret key exist in code that ships to the browser. Concretely:
- Call third-party APIs that require a secret key from a server route or Edge Function, not from client-side code.
- In Next.js, only variables prefixed
NEXT_PUBLIC_are exposed to the browser, everything else stays server-only, but only if you actually read it in server code. A secret key stored without the prefix but still referenced inside a client component gets bundled anyway. - Proxy the request: client calls your server, your server calls the third party with the secret attached, the response comes back without the key ever touching the browser.
Rotate the keys you leaked
Finding a leaked key is not the fix, rotating it is. Once a secret key has been in a public bundle, a public repo, or a source map, assume it has been copied. You have no way to know who has it or how long they have had it.
For each leaked key: generate a new one in the provider's dashboard, update your server environment variables, redeploy, then revoke the old key. Check the provider's usage or audit logs for the affected window for activity you cannot explain.
Prevent it next time
- Add a pre-commit check that greps staged files for known secret prefixes before they can be committed.
- Keep a documented list of which keys on your team are public-safe and which are not, so new engineers do not have to guess.
- Re-run a check like this after every dependency or SDK upgrade, some SDKs default to inlining config in ways that change between versions.