Fix · API endpoints answer without login and secrets ship in the bundle

API key visible in browser JavaScript bundle: how to hide it

You search the built JavaScript and find an API key, or your server's secret names and key formats, in a file the browser downloads. Two common causes are a server config module that a client file imports, and a secret given a public prefix such as NEXT_PUBLIC_ or VITE_.

Likely causes, most common first

Cause 1 · in 1 of 3 matching cases

A server config module is imported by client code

A shared module lists your secret names and the formats it expects for their values. A file that runs in the browser imports it, and the bundler follows the import and writes the module into the client JavaScript. The Next.js docs warn that shared modules make it possible to import server-only code into the client by accident.

How to tell: The file that contains the match is imported, directly or through other modules, by a file marked "use client".

Cause 2 · in 1 of 3 matching cases

A secret carries a public prefix

Next.js can inline NEXT_PUBLIC_ variables into the JavaScript it sends to the browser at build time, replacing references to process.env.NEXT_PUBLIC_X with a hard-coded value. Vite exposes VITE_ variables in client-side source code after bundling, and its guide says the values are bundled into your source code at build time. A key stored under either prefix can therefore be readable in the output.

How to tell: A variable that starts with NEXT_PUBLIC_ or VITE_ holds a key, and a search of the client build output for that key's value finds it.

Cause 3

Environment variables are read across the codebase

Reads of process.env are spread across components, shared helpers and route files. The Next.js data security guide recommends keeping that access in one server-side Data Access Layer, which keeps secrets from being exposed to other parts of the application.

How to tell: A search for process.env returns reads in components and shared helpers, not just one server module.

Check and fix it, step by step

  1. Search the client build output for secret names

    Grep the folder your build writes browser JavaScript to. Search for the names in your env files and the key prefixes your providers use. A match in the client output means that text shipped to browsers.

    grep -rlE 'API_KEY|SECRET' path/to/client-build

    Docs: nextjs.org →

  2. List variables that carry a public prefix

    Look through your env files for names starting with NEXT_PUBLIC_ or VITE_. Vite exposes VITE_ variables in client-side source code, and Next.js can inline NEXT_PUBLIC_ values into the browser bundle, so a key in that list may be exposed. To confirm, search the build output for the key's value.

    grep -rnE '^(NEXT_PUBLIC|VITE)_' .env*

    Docs: vite.dev →

  3. Trace the import that pulled the module in

    Open the file that contains the match and find which module imports it. Follow the chain up to a file marked "use client", because Next.js includes that file's imports in the client bundle.

    Docs: nextjs.org →

  4. Mark the server config module server-only

    Add import 'server-only' at the top of the module that holds secret names and formats. A client import then fails the build instead of shipping. Next.js treats installing the package as optional, but you can install it if your lint rules flag extraneous dependencies.

    import 'server-only'
    
    export const config = {
      apiKey: process.env.API_KEY,
    }

    Docs: nextjs.org →

  5. Split public config from secrets

    Put the values the browser needs in their own module, built from NEXT_PUBLIC_ variables. Read secrets in one server-side layer and return the fields a component needs.

    // lib/public-config.ts: safe for client imports
    export const publicConfig = {
      analyticsId: process.env.NEXT_PUBLIC_ANALYTICS_ID,
    }

    Docs: nextjs.org →

  6. In Vite, drop the prefix and call the provider from a backend

    Rename the variable without the VITE_ prefix and stop reading it in browser code. Have the browser call a backend server or a serverless/edge function that holds the key and talks to the provider.

    console.log(import.meta.env.VITE_SOME_KEY)  // "123", reaches the client
    console.log(import.meta.env.DB_PASSWORD)    // undefined

    Docs: vite.dev →

  7. Revoke the exposed key, then rebuild

    Revoke the key that shipped, issue a new one and set it on the server. Next.js fixes NEXT_PUBLIC_ values at build time, so rebuild and redeploy to replace the old output. Add secrets detection to your repository tooling to catch a repeat.

    Docs: cheatsheetseries.owasp.org →

Quick check: grep -rlE 'API_KEY|SECRET' path/to/client-build # a hit means that text shipped to browsers

How often this shows up in our data

3 of the 701 verified cases from the last 12 months in our data match this symptom (0.4%). The most common cause was “A server config module is imported by client code” (1 of 3); 1 didn't show which cause. How we collect and verify cases.

Questions

Can I keep a key in a NEXT_PUBLIC_ variable?

Next.js can inline these variables into the JavaScript sent to the browser at build time, so treat the value as readable by anyone who loads the page. Keep secrets in unprefixed variables and read them on the server.


How do I make a build fail if client code imports a server module?

Add import 'server-only' to the server module. The Next.js data security guide says this causes a build error if the module is imported in the client environment.


Do I need to rebuild after changing an environment variable?

For NEXT_PUBLIC_ variables, yes. Next.js says that once the app is built, it no longer responds to changes to them, so the values are fixed at build time.


Should I rotate a key that appeared in a public bundle?

Yes. OWASP's secrets management guidance says exposed keys should be revoked immediately, and a new secret should be quick to create and deploy.


Sources

Every cause and step above was checked against these pages on 13 Oct 2026.

The broader pattern

This is one symptom of a wider failure pattern: API endpoints answer without login and secrets ship in the bundle. The guide covers every cause we see for it, on any stack.

Get a heads-up when your stack breaks something

Leave your email and we'll let you know when something big changes for your stack. Unsubscribe any time by replying. Gemmein Limited. Research terms · Privacy

← All fixes