Next.js Applications

Half your Next.js app is private. Do you know which half?

Next.js is the rare framework where architecture is protection: Server Components never leave the server. But everything marked 'use client' ships in full — and every NEXT_PUBLIC_ variable is baked into that bundle as a plain string literal. The risk is not the framework. It is the boundary being fuzzier than teams assume.

Check This First

grep -r "NEXT_PUBLIC_" .next/static

Every match is a value you published. Not sent at runtime — inlined at build time, sitting in the JavaScript every visitor already downloaded.

Server stays serverRSC code never crosses the network. Your strongest boundary.
'use client' shipsThose components land in the browser, whole.
Imports leakOne bad import pulls server code into the client graph.
The Boundary

Next.js protects code by not shipping it — when you let it

Most frameworks hand the browser everything and leave you to deal with it. Next.js gives you a real server/client split, which means the best protection decision you make is architectural: keep sensitive logic in Server Components and server actions, where it is never exposed at all. Obfuscation then covers what genuinely has to run in the browser. Get the boundary wrong, though, and the framework will happily publish your secrets on your behalf.

Server Components — genuinely private

Code in an RSC executes on the server and only its output is streamed. Pricing rules, entitlement logic, and vendor calls placed here are not obfuscated — they are absent. That beats any client-side defence.

'use client' — fully public

The moment a file carries that directive, it and its import graph are compiled into client chunks and downloaded by everyone. Interactivity has a disclosure cost; the question is what you put behind it.

The accidental import

Import a helper holding server logic into a client component and Next.js pulls it across the boundary silently. The server-only package turns that into a build error — use it on anything that must never ship.

The Number One Leak

NEXT_PUBLIC_ is a publishing instruction

The prefix does not mean “available to the client at runtime.” It means “paste this value into the bundle at build time.” Teams add the prefix to silence an undefined in the browser and, in doing so, publish the value permanently to every visitor and every archived copy of that build.

What you wrote
# .env
NEXT_PUBLIC_ANALYTICS_ID=ga-123
NEXT_PUBLIC_API_KEY=sk_live_9f2a...

// component.tsx  ('use client')
fetch(url, { headers: {
  "x-api-key": process.env.NEXT_PUBLIC_API_KEY
}});
What shipped
// .next/static/chunks/page-8f1c.js
fetch(url,{headers:{
  "x-api-key":"sk_live_9f2a..."
}});

// the literal is IN the bundle.
// Obfuscation raises the cost of
// finding it. It does not make
// a published secret unpublished.

Be clear about what protection can and cannot do here: encrypting the string pool means a casual grep for sk_live finds nothing, which is real value against opportunistic scraping. It is not a fix. A secret in a client bundle is compromised by design — move it behind a server action or a route handler, then obfuscate what remains.

Protect the client chunks only
next build                   # -> .next/

# protect what ships to browsers:
#   .next/static/chunks/**
# leave server output alone:
#   .next/server/**

# filenames must stay identical -
# build manifests reference chunks
# by name and hash.
The Order of Operations

Audit the boundary, then protect the client half

  • Move secrets server-side first. No amount of obfuscation un-publishes a key that is already in a bundle. Server actions and route handlers exist for this.
  • Guard the boundary with server-only so an accidental client import fails the build rather than shipping.
  • Protect only .next/static/chunks — the code browsers actually receive. Server bundles gain nothing and only get harder to debug.
  • Never rename or move chunk files. Rewriting contents is fine; the manifests and the runtime loader resolve chunks by filename.
  • Verify hydration and a route transition after protecting — load a page, check the console for mismatch warnings, then navigate client-side into a lazily loaded route.
Layered Defense

Architecture first, then obfuscation, then runtime

Don’t ship it — the absent layer

The only perfect protection. Server Components, server actions, and route handlers keep logic off the wire entirely. Spend your first effort here, not on tooling.

Obfuscation — the comprehension layer

For code that must be interactive. Encrypted strings, flattened control flow, per-build polymorphic decoder, VM bytecode where it counts. See minification vs obfuscation for why next build alone is not this.

Runtime defense — the behavior layer

Runtime defense detects tampering in the delivered bundle and reports it, so a modified client surfaces instead of quietly working against your API.

Frequently Asked

Next.js protection questions, answered

Is NEXT_PUBLIC_ safe for API keys?

Never. The value is inlined into the bundle at build time as a literal. Treat every NEXT_PUBLIC_ variable as published the moment you build.

Do Server Components ship?

No — only their rendered output. That is Next.js’s strongest protection property, and it is free. Use it deliberately.

How do I stop server code leaking client-side?

Add the server-only package to modules that must never ship. An offending import then fails the build instead of silently publishing.

Should I obfuscate the server bundle?

No. It is never exposed, so there is nothing to gain — and you would only make your own stack traces harder to read.

Will hydration still match?

Yes — obfuscation preserves behavior, so the rendered tree is unchanged. Verify with a real page load and watch for console mismatch warnings.

Will chunk loading break?

Not unless filenames change. Manifests resolve chunks by name; rewrite contents, keep the layout, and test a lazy route.

Start Now

Grep your build output for your own secrets

Run grep -r "NEXT_PUBLIC_" .next/static and then search those chunks for anything resembling a key. What you find is what the internet already has. Fix the boundary first, then protect the client code that legitimately has to be there.