How to Secure Claude API Keys in Frontend Apps
If you're asking how to secure Claude API keys in frontend apps, the short answer is: you can't, not directly, and you shouldn't try. Any secret key embedded in JavaScript that runs in a browser, mobile app, or desktop client is visible to anyone who opens dev tools, inspects network traffic, or decompiles the bundle. There is no obfuscation technique, environment variable trick, or build-time substitution that keeps a frontend-exposed key private.
The real fix isn't "securing" the key in the frontend — it's never putting it there in the first place. This article covers the architecture that actually works: a backend layer between your UI and the Claude API, how to scope and rotate credentials, and what to do if a key has already leaked.
Why frontend-stored keys are never safe
A few things people try, and why they fail:
- Environment variables in a client-side build. Tools like Vite or Create React App inline
.envvalues into the compiled JavaScript at build time. Anyone can read them from the shipped bundle. - Fetching the key from a "protected" endpoint at runtime. If the browser can retrieve it, so can a script that mimics the browser's requests.
- Obfuscating or encrypting the key client-side. The decryption logic has to run in the browser too, which means the key is reconstructable. This slows down a casual observer for maybe five minutes.
- Restricting by domain/referer headers. These are client-supplied and trivially spoofed with a tool like curl or Postman.
Once a key is exposed, it can be used by anyone, from anywhere, on your billing account. For a pay-per-token API like Claude's, that's a direct financial risk, not just a theoretical security issue.
The correct pattern: a backend proxy
Every production app calling Claude needs a server-side component that holds the credential. The frontend never talks to Anthropic or any Claude-compatible API directly — it talks to your backend, and your backend talks to the model.
Browser/App ---> Your Backend ---> Claude API
(no secret) (holds secret)
Minimal example of what that backend route looks like in Node.js:
// server.js
app.post("/chat", async (req, res) => {
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
messages: req.body.messages,
}),
});
const data = await response.json();
res.json(data);
});
The frontend calls /chat on your own domain, authenticated with your app's normal session/auth system (JWT, cookie, API key issued to your own users) — never with a Claude-facing credential. The actual model key lives only in server environment variables, a secrets manager, or your deployment platform's encrypted config, and is read once at process start.
This is also where SubToAPI fits if you're building on top of Claude access rather than a direct Anthropic account: you issue sub_live_... application keys from your dashboard, keep that key server-side exactly like the Anthropic key above, and your backend forwards requests to https://api.subtoapi.app/v1/messages. The pattern doesn't change — the secret still never reaches the browser. See the quickstart for the full setup.
Scoping and rotating credentials
Holding the key server-side solves exposure, but you should still limit blast radius if that server is ever compromised:
- Use per-environment keys. Separate keys for local dev, staging, and production so a leaked dev key doesn't touch production billing.
- Use per-app or per-team keys where supported. SubToAPI issues keys per team seat, so you can revoke one integration or one developer's access without rotating everyone's credentials. See docs for key management.
- Rotate on a schedule and on suspicion. Treat any key committed to git history, pasted into a support ticket, or logged in plaintext as compromised — rotate immediately, don't wait.
- Set spend or rate limits where your provider allows it. This caps damage even if a key does leak, by capping how much it can cost you before you notice.
Handling streaming and tool use without exposing secrets
Streaming responses and tool-use flows don't change the security model — they just need your proxy to pass chunks through instead of waiting for a full response.
app.post("/chat/stream", async (req, res) => {
const upstream = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ...req.body, stream: true }),
});
res.setHeader("Content-Type", "text/event-stream");
for await (const chunk of upstream.body) {
res.write(chunk);
}
res.end();
});
The browser receives streamed tokens over a connection to your own server; it never holds or sees the upstream Authorization header. The same applies to tool use — your backend executes or routes tool calls and only returns results to the client. Details on both flows are in the streaming docs and tools docs.
A practical checklist
- [ ] No Claude-facing key appears in any client bundle, mobile binary, or browser storage (localStorage, cookies, sessionStorage).
- [ ] All model calls route through a backend you control.
- [ ] The frontend authenticates to your backend with your own app's auth, not a model provider key.
- [ ] Keys are stored in environment variables or a secrets manager, never committed to source control.
- [ ] Separate keys exist per environment and, ideally, per team or app.
- [ ] You have a rotation process and actually use it when a key might be exposed.
- [ ] Rate or spend limits are configured where the provider supports them.
If you're starting from scratch, wiring up the proxy pattern yourself is straightforward but adds a chunk of infrastructure (auth, rate limiting, logging, retries) before you've written a line of product code. That's the gap SubToAPI is built for: you get a hosted API layer with scoped sub_live_... keys, usage metadata, and team seats, so the "secure backend" piece is already done — you just call the endpoint from your existing server. Start with a free trial at signup.
Questions
Can I use CORS restrictions instead of a backend proxy to protect my Claude key? No. CORS only controls which origins a browser allows to read a response; it does nothing to hide a key embedded in your JavaScript, and non-browser clients ignore CORS entirely. A key visible in the frontend is compromised regardless of CORS settings.
Is it safe to use a serverless function as the backend proxy instead of a traditional server? Yes — a serverless function (Vercel, AWS Lambda, Cloudflare Workers) is a perfectly valid backend for this pattern, as long as the key is stored in its encrypted environment config and never returned in a response body to the client.
What should I do if I accidentally committed a Claude or SubToAPI key to a public GitHub repo? Rotate the key immediately from your provider dashboard, then remove it from git history (not just a new commit) using a tool like git filter-repo, and check usage logs for unexpected activity during the exposure window.