Threat modeling

You can’t hide an API key in JavaScript

We sell an obfuscator, so this may be the last thing you expect us to say: obfuscation will not protect a secret you ship to the browser. Nothing will. A key that reaches client-side JavaScript is already public, because the browser has to be able to read it — and anything the browser can read, so can the person holding the browser. This is worth being clear about, because “obfuscate the key” is one of the most common wrong answers in front-end security, and it fails in production in a way that is expensive to discover.

Why “public” is the right word

A secret is only secret if the people who shouldn’t have it can’t get it. When your code does this:

fetch("https://api.vendor.com/v1/charge", {
  headers: { "Authorization": "Bearer sk_live_9f2a..." }
});

the browser must hold that token in memory to send the request, and the request itself goes out over the wire with the token in a header. An attacker doesn’t even need to read your code — they open the Network tab and watch the request leave. The key is not hidden in the bundle; it is broadcast on every call.

What obfuscation actually does to a key (and what it doesn’t)

Let’s be precise, because there is a real effect — it is just not the one people hope for. Obfuscating a bundle that contains sk_live_9f2a... with string encryption means a casual grep for sk_live finds nothing, and a scraper harvesting keys from public bundles at scale moves on to an easier target. That is genuine value against opportunistic, low-effort theft.

It is not a fix. A motivated attacker sets one breakpoint on the network call, or logs the decrypted string pool, and reads the key in seconds. Encryption only relocates the problem: to decrypt the key at runtime, the decryption routine and its key must also ship in the bundle — so you have shipped the lock and the key taped to it. A secret in client code is compromised by design; obfuscation changes the price of the attack from “free” to “a few minutes,” not from “possible” to “impossible.”

The trap with a friendly name: NEXT_PUBLIC_ and friends

Framework conventions make this worse by looking safe. In Next.js, a NEXT_PUBLIC_ environment variable is inlined into the client bundle as a literal at build time — the prefix does not mean “available at runtime,” it means “paste this value into the JavaScript every visitor downloads.” Teams add the prefix to silence an undefined in the browser and, in doing so, publish the value permanently. Vite’s VITE_, Create React App’s REACT_APP_, and most bundlers’ define plugins do the same thing. If a secret has one of those prefixes, treat it as leaked. (More on the Next.js specifics in protecting Next.js apps.)

The one pattern that works: keep the secret on the server

The fix is boring and total. The browser should never see the secret at all. Instead of calling the vendor directly, the browser calls your server, and your server — where the key lives and never leaves — makes the authenticated call:

// browser: no secret, calls your own endpoint
fetch("/api/charge", { method: "POST", body: JSON.stringify(order) });

// your server (server action / route handler / API):
// the key is read from a server-only environment variable
// and is never sent to the client.
fetch("https://api.vendor.com/v1/charge", {
  headers: { "Authorization": `Bearer ${process.env.VENDOR_SECRET}` }
});

This also gives you the things a raw client-side key never could: you can rate-limit, validate, log, and revoke at your own boundary. If a vendor genuinely has no server-side option and forces a browser key, treat that key as public by design — scope it to the narrowest permissions and lock it to your domain/referrer in the vendor dashboard, and assume it will be reused elsewhere.

So where does obfuscation legitimately help?

With the thing that genuinely only exists on the client: your logic. Secrets belong on the server; but your pricing rules, licensing checks, anti-abuse heuristics, proprietary algorithms, and the shape of your app all have to run in the browser to do their job. That code cannot be moved server-side without becoming a different product — and that is exactly what obfuscation is for. It raises the cost of reading, copying, and tampering with the logic you have no choice but to ship.

Put simply: move secrets off the client; obfuscate the logic that must stay. The two are complementary, and confusing them is where teams go wrong.

The thing to check today

Search your deployed bundles for your own keys — grep -riE "sk_live|api[_-]?key|secret" dist — and open the Network tab on your running app to see which credentials leave the browser. Anything you find is already public; move it behind a server endpoint. Then take the client logic that legitimately remains and see what the obfuscator does to it.