This one takes about ninety seconds to check on your own project, and I'd genuinely encourage you to stop reading and go do it before finishing this post, because if you have this bug, it's live in production right now, not a theoretical risk.
Open your deployed site. Open dev tools. Go to the Network tab, find any loaded .js file from _next/static, and search it for the word sk_ or SECRET or the name of any API key you use. If you find one, that key is not hidden. It is sitting in plain text in a file anyone visiting your site can download.
Why This Happens
Next.js has a genuinely useful convention, any environment variable prefixed with NEXT_PUBLIC_ gets exposed to the browser, inlined directly into the client-side JavaScript bundle at build time. This exists for real, legitimate reasons, a Stripe publishable key, an analytics ID, anything that's meant to be public and used client-side.
The bug happens when a developer, often under deadline pressure or copy-pasting a snippet from a tutorial, prefixes a secret with NEXT_PUBLIC_ without realizing what that prefix actually does. It's an easy mistake to make precisely because the variable still works, the app functions perfectly, nothing throws an error, nothing looks broken. The secret is just quietly baked into a public file instead of staying server-only.
What This Actually Looks Like
# .env.local
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_... # ❌ This is now public
STRIPE_SECRET_KEY=sk_live_... # ✅ This stays server-only
// This code looks completely normal
const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY as string);
Nothing about this code visually signals danger. It compiles, it runs, Stripe charges process correctly. The only thing wrong is the prefix, and that one prefix is the entire difference between a secret staying on your server and that same secret being downloadable by literally anyone who loads your site.
Why This Specific Mistake Is So Easy to Make
A huge number of tutorials, especially older ones or quick copy-paste snippets from Stack Overflow and AI chat tools, use NEXT_PUBLIC_ prefixes liberally without explaining the actual security implication, because in the tutorial's specific example, the value genuinely was meant to be public. Someone adapting that pattern to a different variable, a real secret this time, can carry the prefix over without registering that it changes the entire security model of that value.
AI coding tools make this specific mistake easier to introduce too. Asked to "add a Stripe integration," a tool might generate an example using NEXT_PUBLIC_ for consistency with a publishable key example it saw elsewhere, without any awareness that this particular key was meant to stay secret. The code works. The demo looks perfect. Nothing fails until someone specifically checks the bundle.
The Actual Rule
Only ever prefix an environment variable with NEXT_PUBLIC_ when the value is explicitly meant to be visible to anyone, a publishable key, a public analytics ID, a public API endpoint URL. Anything that grants access, proves identity, or authorizes an action, secret keys, database URLs, private API tokens, webhook secrets, session signing secrets, stays unprefixed, full stop, no exceptions for convenience.
// ✅ Genuinely public values, safe with NEXT_PUBLIC_
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-...
NEXT_PUBLIC_API_URL=https://api.example.com
// ✅ Genuinely private values, never prefixed
STRIPE_SECRET_KEY=sk_live_...
MONGODB_URI=mongodb+srv://...
JWT_SECRET=...
RESEND_API_KEY=...
How to Actually Audit an Existing Project
Grep your entire codebase for the prefix and manually review every match, this takes a few minutes and is worth doing on any project you didn't build from scratch, or haven't specifically checked before:
grep -r "NEXT_PUBLIC_" --include="*.ts" --include="*.tsx" .
For each result, ask honestly: would it actually be fine if a random visitor downloaded and read this exact value right now. If the answer is no, that variable needs the prefix removed, and depending on what it is, the underlying key or secret itself may need to be rotated, since if it's already been deployed with that prefix, assume it has already been exposed and treat it as compromised, not just misconfigured.
What to Do If You Find One
Removing the prefix going forward is not enough on its own. If a secret was ever deployed with NEXT_PUBLIC_, it has been sitting in a publicly downloadable file for however long that deploy was live, and it may be cached in browsers, crawled by bots, or archived by services that snapshot public JavaScript bundles. Rotate the actual key at the provider (Stripe, your database, wherever it originated), not just the environment variable name.
If you just went and checked your own bundle because of this post, genuinely let me know what you found, clean, or a nervous rotation happening right now. Either answer is useful to hear, curious how common this actually is in the wild versus how careful people generally already are.
Get the templates: https://pixelanas.gumroad.com
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (1)
Grepped mine just now, clean. Worth adding to the audit step though, TypeScript won't save you here since process.env.ANYTHING types as string regardless of whether the value is meant to be public, there's no compiler signal telling you the prefix is wrong. It's a pure human review problem, which is exactly why it slips through under deadline pressure like you said.