How to Prevent API Key Leaks in Frontend Apps
If you're asking how to prevent API key leaks in frontend code, the short answer is: you can't keep a secret in JavaScript that ships to the browser. Any key, token, or credential bundled into client-side code — React, Vue, a static site, a mobile webview — is visible to anyone who opens dev tools, views page source, or intercepts network traffic. Minifying it, obfuscating it, or storing it in an environment variable prefixed with REACT_APP_ or NEXT_PUBLIC_ doesn't hide it; it just makes it slightly harder to find at a glance.
The real fix isn't a clever hiding technique. It's architectural: never let the browser hold a credential that has direct access to a paid or sensitive API. Instead, put a server between the frontend and the third-party API, and let the frontend talk to your server with a scoped, revocable token instead. Below is a breakdown of why leaks happen, how they actually get exploited, and the patterns that stop them.
Why Frontend API Keys Leak
Every request your browser makes is visible in the Network tab. Every bundled JS file is downloadable and readable. This means:
- Build-time env vars aren't secrets. Tools like Vite, Create React App, and Next.js explicitly warn that any variable exposed to the client is public.
VITE_API_KEYorNEXT_PUBLIC_API_KEYgets inlined into the JS bundle at build time — it's just a string in a file. - Obfuscation doesn't work. Base64-encoding a key, splitting it across variables, or loading it from a "hidden" JSON file are all trivially reversible. Attackers grep bundles for common key patterns (
sk-,AIza,sub_live_) as a matter of routine. - Source maps leak keys too. Even if you strip keys from minified code, an accidentally deployed
.js.mapfile can expose the original source with the key intact. - Scrapers and bots actively look for this. GitHub, npm, and public deployments get scanned continuously for exposed credentials. A key leaked in a commit or a live site can be found and abused within minutes.
Once a key leaks, anyone can use it — running up your bill, exhausting rate limits, or accessing data you didn't intend to expose.
The Core Fix: Never Ship Real Credentials to the Client
The only reliable pattern is to move the credential server-side and give the browser something disposable instead.
1. Use a backend proxy
Route all calls to the sensitive API through your own server. The browser calls your API; your server holds the real key and forwards the request.
// server.js — the real key never leaves the server
app.post("/api/chat", async (req, res) => {
const response = await fetch("https://api.example.com/v1/chat", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PROVIDER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(req.body),
});
const data = await response.json();
res.json(data);
});
The frontend never sees PROVIDER_API_KEY. It only talks to /api/chat, which you fully control.
2. Issue short-lived, scoped tokens instead of raw keys
If you need the browser to authenticate directly (for streaming, for example), don't hand out the provider's master key. Issue a short-lived token scoped to a single user and a narrow set of actions, generated by your backend after it verifies the request.
3. Use per-application keys, not one shared master key
If your provider supports it, generate a distinct API key per application or per environment instead of reusing one key everywhere. That way, if a key does leak, you can revoke just that one without breaking every integration. This is exactly the model SubToAPI uses: instead of exposing your raw Claude access, you generate application keys (sub_live_...) scoped per app, rotate or revoke them individually from the dashboard, and route requests through https://api.subtoapi.app/v1/messages from your backend. Your actual Claude account credentials never touch the frontend at all — see the quickstart for the setup.
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-3-5-sonnet",
"max_tokens": 512,
"messages": [{"role": "user", "content": "Summarize this ticket"}]
}'
That call belongs on your server, called from a route your frontend hits — not embedded in client JS.
4. Set spending and rate limits at the key level
Even with a proxy in place, defense in depth matters. Cap usage per key so a compromised token (server-side or otherwise) has a low ceiling. Combine this with request logging so you notice anomalies — a spike in calls from one key or IP is often the first sign of a leak, not the last.
5. Rotate and revoke on a schedule, not just after an incident
Treat key rotation as routine maintenance, not emergency response. If a key was ever pasted into a Slack message, a .env committed by mistake, or a support ticket, rotate it immediately — don't wait to see if it gets abused.
A Practical Checklist
- [ ] No API keys in
NEXT_PUBLIC_,VITE_,REACT_APP_*, or any client-bundled env var - [ ] All third-party API calls go through your backend, never directly from the browser
- [ ]
.envfiles are in.gitignoreand never committed - [ ] Source maps are excluded from production deploys or served only internally
- [ ] Each application/environment has its own key, not a shared master key
- [ ] Usage limits and alerts are set per key
- [ ] A rotation process exists and is actually used
If you're building on top of Claude specifically, pairing this checklist with per-app keys and a hosted proxy (like the one described in the streaming docs or tool use docs) removes most of the manual plumbing — you get the scoping and revocation without writing the proxy yourself.
Frequently asked questions
Can I just hide the API key with obfuscation or Base64 encoding? No. Obfuscation and encoding are reversible in seconds by anyone with dev tools open. They don't count as security — only removing the key from client code entirely does.
Is it safe to use environment variables in a React or Next.js app? Only for non-secret configuration like public URLs or feature flags. Any env var exposed to the client (NEXT_PUBLIC_, REACT_APP_, VITE_*) is bundled into the JS and publicly visible — it's not a safe place for API keys.
What should I do if a key has already leaked? Revoke or rotate it immediately, check usage logs for abnormal activity during the exposure window, and move the integration behind a backend proxy so the new key never reaches the client again.