Your app can be perfectly functional, beautifully designed, deployed on Vercel, passing every test—and still have a serious security problem hiding inside its JavaScript bundle.
And checking for it takes about 90 seconds.
Open your production website.
Open DevTools → Sources or Network.
Find a JavaScript file under something like:
/_next/static/...
Search inside the loaded JavaScript for:
SECRET
API_KEY
sk_
mongodb
password
token
If you discover a private credential in there, stop.
That value isn't "kind of exposed."
It is public.
Anyone who can visit your website can download the same JavaScript and inspect it.
The scary part?
Your application may continue working perfectly.
The NEXT_PUBLIC_ Prefix Is Not Just a Naming Convention
This is one of those Next.js features that is extremely useful when you understand it—and extremely dangerous when you don't.
Consider:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
That's fine.
A Stripe publishable key is designed to be used by the browser.
But this?
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_...
That's a completely different story.
The NEXT_PUBLIC_ prefix tells Next.js that the value can be exposed to client-side code.
In other words, you're effectively saying:
"It's okay to put this value into code that every visitor can download."
That's perfectly reasonable for public configuration.
It's disastrous for credentials.
The Dangerous Version
Imagine you have this:
# .env.local
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_123456789
And somewhere in your application:
const stripe = new Stripe(
process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY as string
);
Everything might appear normal.
Your build succeeds.
Your page loads.
Your Stripe integration works.
There might be no error in the console.
Nothing visually tells you that you've just moved a secret from a protected server environment into a browser-downloadable bundle.
That's what makes this class of mistake so dangerous.
The bug doesn't necessarily break your application.
It breaks your security boundary.
What Should Be Public vs Private?
A useful mental model is this:
Public
These values are generally expected to be visible to users:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-...
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_API_URL=https://api.example.com
Someone seeing these values should not gain unauthorized access to your systems.
Private
These values should stay on the server:
STRIPE_SECRET_KEY=sk_live_...
DATABASE_URL=postgresql://...
MONGODB_URI=mongodb+srv://...
JWT_SECRET=...
RESEND_API_KEY=...
WEBHOOK_SECRET=...
AWS_SECRET_ACCESS_KEY=...
If revealing a value would allow someone to:
- authenticate as your application
- access a database
- send email through your account
- create charges
- access private APIs
- impersonate users
- verify or forge signed data
- modify cloud resources
then it does not belong in a client bundle.
Here's the Rule I Use
Don't ask:
"Does my frontend need this value?"
Ask:
"Would I be comfortable publishing this value on my homepage?"
If the answer is no, it should not be in client-side JavaScript.
This simple question catches a surprising number of configuration mistakes.
But There’s Another Trap
You might think:
"Fine. I'll just remove
NEXT_PUBLIC_."
That's necessary, but sometimes it isn't sufficient.
Consider:
NEXT_PUBLIC_RESEND_API_KEY=re_123456
You discover it in production and change it to:
RESEND_API_KEY=re_123456
You deploy again.
The new deployment is better.
But the old key was already exposed.
It may have appeared in:
- browser caches
- downloaded JavaScript
- monitoring systems
- crawlers
- screenshots
- logs
- third-party archives
- someone else's local copy
You don't know who has seen it.
So if a real credential has already been shipped to the browser, treat it as compromised.
Rename the environment variable and rotate the credential.
That's the important part many quick fixes miss.
How I Audit a Next.js Project
Start with your environment variables.
Search for every NEXT_PUBLIC_ value.
grep -r "NEXT_PUBLIC_" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" .
On Windows, you can use PowerShell:
Get-ChildItem -Recurse -Include *.ts,*.tsx,*.js,*.jsx |
Select-String "NEXT_PUBLIC_"
Then review every result.
Don't automatically assume every NEXT_PUBLIC_ variable is dangerous.
Instead, ask:
"Would this value still be safe if a stranger copied it?"
If yes, you're probably fine.
If no, move it server-side.
Don't Forget Your Source Code
Environment variables aren't the only place secrets accidentally end up.
Search your repository for suspicious patterns too:
grep -rE "sk_live_|sk_test_|API_KEY|SECRET_KEY|PASSWORD|TOKEN" .
And check for things developers commonly forget:
const config = {
apiKey: "123456",
secret: "super-secret-value",
};
Hardcoded credentials are still credentials.
Putting them in a .env file doesn't magically make them private either.
The important question is where the value ultimately reaches.
Server Components Don't Automatically Mean Everything Is Safe
This is another important distinction in modern Next.js.
You can safely access private environment variables in server-side code:
const users = await db.user.findMany();
using something like:
DATABASE_URL=...
But the moment you deliberately pass sensitive information to a Client Component, you've crossed the boundary.
For example:
<ClientComponent secret={process.env.SECRET_KEY} />
That's a problem.
The component runs in the browser.
Whatever data you send to it needs to be considered browser-visible.
The server/client boundary is more important than the filename or where the environment variable originally came from.
A Better Architecture
Instead of sending a secret to the browser:
Browser
↓
Secret API key
↓
Third-party API
keep the credential on the server:
Browser
↓
Your Next.js server
↓
Private API key
↓
Third-party API
For example:
// Server-side route
const response = await fetch("https://api.example.com/data", {
headers: {
Authorization: `Bearer ${process.env.PRIVATE_API_KEY}`,
},
});
The browser gets the result.
It doesn't get the credential.
That's the security boundary you want.
What About NEXT_PUBLIC_API_URL?
This one causes confusion because the answer isn't always "never."
For example:
NEXT_PUBLIC_API_URL=https://api.example.com
is usually fine.
Knowing the address of your API doesn't inherently give someone access to it.
But this:
NEXT_PUBLIC_API_TOKEN=super-secret-token
is very different.
The URL can be public.
The credential should not be.
Public endpoint ≠ private credential.
What About Stripe?
Stripe is a good example because their architecture intentionally has both public and private keys.
A browser can use:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
But your server should use:
STRIPE_SECRET_KEY=sk_live_...
Never swap those simply because "Stripe needs a key on the frontend."
It needs a publishable key on the frontend.
The secret key belongs on the server.
That distinction matters.
What About Database Credentials?
This should be an immediate red flag:
NEXT_PUBLIC_DATABASE_URL=mongodb+srv://...
No.
Your browser should not know your database connection string.
Not even if:
"The frontend needs to fetch some data."
The frontend needs the data.
It doesn't need the database credentials.
Your server should handle that communication.
The 90-Second Production Check
If you maintain a Next.js application, here's a quick check worth doing today.
Step 1
Open your production website.
Step 2
Open DevTools.
Step 3
Go to Network.
Step 4
Reload the page.
Step 5
Find a JavaScript file under:
_next/static/
Step 6
Search the response for suspicious strings:
sk_live_
sk_test_
SECRET
PASSWORD
DATABASE
mongodb
TOKEN
PRIVATE
You may find nothing.
That's good.
But if you find a real credential?
Don't just delete the variable and move on.
Rotate it.
Then investigate where it was exposed and for how long.
The Bigger Lesson
This isn't really a Next.js problem.
It's a trust-boundary problem.
The browser is not a trusted environment.
Anything shipped to the browser should be considered potentially visible to the user.
Minifying JavaScript doesn't make a secret private.
Obfuscating it doesn't make it private.
Encoding it doesn't make it private.
Putting it in an environment variable doesn't make it private.
And hiding it behind a React component doesn't make it private.
If the browser receives it, assume the browser user can eventually see it.
That's the mental model that makes frontend security much easier to reason about.
My Simple Environment Variable Checklist
Before shipping a Next.js application, I like to ask:
✓ Is this value intentionally public?
✓ Does exposing it grant access to anything?
✓ Is it being imported into client-side code?
✓ Is it being passed into a Client Component?
✓ Does it authenticate a request?
✓ Does it contain a database credential?
✓ Does it sign or verify anything?
✓ Would I rotate it if it appeared on GitHub?
If you answer "yes" to that last question, it probably shouldn't be public.
One Prefix. Completely Different Security Model.
That's the part I think developers should remember.
These two variables look almost identical:
NEXT_PUBLIC_STRIPE_KEY=...
and:
STRIPE_KEY=...
But they're not equivalent.
One is explicitly intended for browser exposure.
The other can remain server-side.
A tiny naming difference can change the security boundary of your entire application.
So before your next production deploy, take 90 seconds.
Check the bundle.
You might discover nothing.
And honestly, that's the best possible outcome.
But if you do discover a secret, finding it yourself today is much better than finding out about it from your billing dashboard, database logs, or a very uncomfortable message from a client.
Have you ever accidentally exposed an API key through a frontend bundle?
Or have you found a secret in a project someone else built?
I'm genuinely curious how common this is in real-world Next.js projects. Drop your experience in the comments.
Get the templates: https://pixelanas.gumroad.com
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (0)