DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Your Next.js App Might Be Leaking Secrets Right Now. Check Your Client Bundle.

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/...
Enter fullscreen mode Exit fullscreen mode

Search inside the loaded JavaScript for:

SECRET
API_KEY
sk_
mongodb
password
token
Enter fullscreen mode Exit fullscreen mode

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_...
Enter fullscreen mode Exit fullscreen mode

That's fine.

A Stripe publishable key is designed to be used by the browser.

But this?

NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And somewhere in your application:

const stripe = new Stripe(
  process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY as string
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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=...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You discover it in production and change it to:

RESEND_API_KEY=re_123456
Enter fullscreen mode Exit fullscreen mode

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" .
Enter fullscreen mode Exit fullscreen mode

On Windows, you can use PowerShell:

Get-ChildItem -Recurse -Include *.ts,*.tsx,*.js,*.jsx |
  Select-String "NEXT_PUBLIC_"
Enter fullscreen mode Exit fullscreen mode

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" .
Enter fullscreen mode Exit fullscreen mode

And check for things developers commonly forget:

const config = {
  apiKey: "123456",
  secret: "super-secret-value",
};
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

using something like:

DATABASE_URL=...
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

keep the credential on the server:

Browser
   ↓
Your Next.js server
   ↓
Private API key
   ↓
Third-party API
Enter fullscreen mode Exit fullscreen mode

For example:

// Server-side route

const response = await fetch("https://api.example.com/data", {
  headers: {
    Authorization: `Bearer ${process.env.PRIVATE_API_KEY}`,
  },
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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_...
Enter fullscreen mode Exit fullscreen mode

But your server should use:

STRIPE_SECRET_KEY=sk_live_...
Enter fullscreen mode Exit fullscreen mode

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://...
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

Step 6

Search the response for suspicious strings:

sk_live_
sk_test_
SECRET
PASSWORD
DATABASE
mongodb
TOKEN
PRIVATE
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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=...
Enter fullscreen mode Exit fullscreen mode

and:

STRIPE_KEY=...
Enter fullscreen mode Exit fullscreen mode

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)