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.

Frequently asked questions

Can any obfuscation option keep an API key secret in browser code?

No, and the reason is structural rather than a limitation of any particular option. Whatever your code does to reconstruct the key, the browser has to be able to do it too, because otherwise the request would not work. That means every step needed to recover the value is present in the material you shipped. String encryption, string tables and encoding all raise the effort of finding it by reading, and none of them changes the fact that the value is recoverable by running the code or by watching the request that carries it.

What about encrypting the key and decrypting it at run time?

Then you have shipped the ciphertext and the decryption routine in the same file. Anyone who can run your code can run the decoder, and the usual approach is not even to read the decoder but to let it run and observe the result. It is a real increase in effort compared with a plain literal, which is worth having, but it is a cost increase rather than a confidentiality property.

Would the network request give it away anyway?

Usually, yes, and this is the part people forget when focusing on the bundle. Even a perfectly hidden value has to be attached to an outgoing request, where the browser's own network panel shows it in full. So the effort spent making the value hard to find in the file is bounded by how easy it is to read off the wire, which is trivial.

What is the correct arrangement?

Move the credential to a server you control and let the browser call your endpoint instead of the third party. Your server holds the key, authenticates the user, applies whatever rules and rate limits you want, and forwards the request. The browser never possesses the secret, so there is nothing in the bundle to protect.

Are there keys that are genuinely fine to ship?

Yes, and distinguishing them saves a lot of wasted effort. Some values are publishable by design: identifiers meant to be public, keys whose provider expects them to appear in client code and which are constrained by referrer or origin restrictions configured at the provider. The test is what an attacker gains by taking the value. If the answer is nothing beyond what they could do from your own page, it is an identifier rather than a secret.

So is there any point protecting client code that talks to an API?

Yes, but for different reasons than secrecy. Protection raises the cost of understanding your request shapes, your endpoint structure and your client-side business rules, which is genuine friction against reconnaissance and copying. What it does not do is turn a shipped value into a confidential one, and the two benefits should not be described as though they were the same thing.

Related reading: Does obfuscation break Web Crypto? · Does obfuscation break URL and query string building? · Your regular expressions are not obfuscated · Zero trust and client-side JavaScript · Obfuscation and short-lived access tokens · Your WebRTC credentials are in your bundle · Your random numbers run on their machine · Your source maps are publishing your source code · Can you lock JavaScript to a device? · Obfuscation vs encryption for JavaScript · Is JavaScript obfuscation legal? · Does obfuscation stop web scrapers? · Your bundle is leaking your roadmap · Obfuscation and the insider threat · Obfuscation and trade secrets · Obfuscation, passkeys and WebAuthn · Obfuscation does not protect browser storage · Protecting a Web3 dApp frontend · Obfuscation will not hide your GraphQL API · Your WebSocket protocol is in your bundle · Your AI system prompt is in your bundle · obfuscation will not fix a vulnerability · Your WebGL shaders are not obfuscated · Your machine learning model is not obfuscated · Your pricing logic is in your bundle · Your JavaScript cannot authenticate a payment.