Secret API key in frontend code (OpenAI, Anthropic, Stripe, Resend): how to fix it
By Obscura Security · Updated · OpenAI, Anthropic, Stripe, Resend, Next.js, Vite, Lovable, Bolt
Short answer
Any key in the browser's JavaScript can be copied by every visitor. If an OpenAI, Anthropic, Stripe secret or Resend key is in your frontend (often through a VITE_ or NEXT_PUBLIC_ variable), revoke it, create a new one, call the provider from a server route, and set a spend limit.
If you can search your site’s JavaScript and find a string like sk-proj-..., sk-ant-..., sk_live_... or re_..., anyone else can find it too.
Why this happens
Build tools separate “server” variables from “public” ones by their name. Variables starting with VITE_ (Vite, which Lovable and Bolt projects use) or NEXT_PUBLIC_ (Next.js) are written into the JavaScript bundle at build time, on purpose, so the browser can read them. Other tools have the same rule with different prefixes: REACT_APP_, PUBLIC_ (Astro, SvelteKit) and EXPO_PUBLIC_.
AI coding tools often add that prefix to make an error go away. The app then works, because the browser now has the key, and the key is now public.
A stolen OpenAI or Anthropic key gets used to run someone else’s workload on your bill. A leaked Stripe secret key can read customer and payment data and, depending on its permissions, issue refunds. A leaked Resend key can send email from your domain.
How to check your own app
Do this on your own site and code.
- Search your env files and code for secret-looking names and values:
grep -rnE "(VITE|NEXT_PUBLIC|REACT_APP|PUBLIC|EXPO_PUBLIC)_[A-Z_]*(KEY|SECRET|TOKEN)" . \
--exclude-dir=node_modules --exclude-dir=.git
Then look at each hit. A Stripe publishable key is fine. An OpenAI key is not.
-
Search what visitors receive. Open your live site, open the browser developer tools, go to Sources, and search across all files for
sk-,sk_live_,rk_live_andre_. Or run the samegrepon your build output (dist,buildor.next/static). -
Check your Network tab. If your browser sends requests straight to
api.openai.com,api.anthropic.comorapi.resend.comwith anAuthorizationheader, the key is in the browser. -
Check your repo history with
git log -p -S "sk-" --all, since a key deleted from the current code may still be in old commits.
The fix
1. Revoke and replace the key
Do this before you tidy the code. In the provider’s dashboard, open the API keys page, create a new key, and delete the exposed one:
- OpenAI: the API keys page in your project settings.
- Anthropic: API Keys in the Console.
- Stripe: Developers → API keys. Stripe lets you roll a secret key, and you can set the old key to expire after a short delay. Consider a restricted key (
rk_) with only the permissions you need. - Resend: the API Keys page.
Deleting the old key is the step that matters, because anyone who already copied it can keep using it until it’s gone.
2. Move the call to the server
The browser should call your own endpoint, and your endpoint calls the provider. In Next.js that’s a route handler. The key lives in a variable with no public prefix:
// app/api/chat/route.ts
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
export async function POST(req: Request) {
// 1. Check the user is logged in (use your auth library here).
// 2. Rate limit them.
const { message } = await req.json()
if (typeof message !== 'string' || message.length > 2000) {
return Response.json({ error: 'Invalid message' }, { status: 400 })
}
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: message }],
max_tokens: 500,
})
return Response.json({ reply: completion.choices[0].message.content })
}
The frontend then calls fetch('/api/chat', { method: 'POST', body: ... }) and never sees the key. Set OPENAI_API_KEY in your hosting platform’s environment settings (for example Vercel’s Project Settings → Environment Variables), not in a public-prefixed variable.
If your app is a plain Vite single-page app with no backend, add one. Options include a Vercel or Netlify serverless function, a Cloudflare Worker, or a Supabase Edge Function. They all let you keep a secret in an environment variable.
The same pattern applies to Anthropic and Resend: create the client in server code only, using a variable such as ANTHROPIC_API_KEY or RESEND_API_KEY.
3. Protect the endpoint you just created
Moving the key doesn’t stop strangers calling your endpoint and running up your bill. Require a logged-in user, limit requests per user, and cap input and output length as in the example above. See no rate limit on login or AI endpoint.
4. Set spend limits and alerts
Open the billing or limits settings at your AI provider and set a monthly budget and an alert. OpenAI and Anthropic both offer usage limits in their dashboards. Stripe and Resend have no spend to cap in the same way, so for those rely on restricted keys and on checking activity logs.
5. Check what happened
Look at the provider’s usage or logs page for activity you don’t recognise, particularly in the period the key was exposed.
Mistakes to avoid
- Renaming the variable. Changing
VITE_OPENAI_KEYto something else but still reading it in browser code changes nothing. - Hiding the key with encoding. Base64 or splitting the string is not protection. The browser has to decode it to use it.
- Fixing it but keeping the old key alive. If you don’t revoke it, the exposed copy still works.
- Letting the AI tool “move it to the backend” without checking. Open the Network tab afterwards and confirm no request to the provider leaves the browser.
Common questions
- Why is my API key visible if I put it in a .env file?
- A .env file is only private if the value stays on the server. Variables whose names start with VITE_, NEXT_PUBLIC_, REACT_APP_, PUBLIC_ or EXPO_PUBLIC_ are deliberately copied into the JavaScript that every visitor downloads. The .env file is safe, but the build output is not.
- Which keys are safe to put in the frontend?
- Keys designed to be public, such as a Stripe publishable key (pk_live_ or pk_test_), a Supabase anon or publishable key, or a Firebase config. Anything that can spend money, send email or read private data is a secret key and belongs on the server.
- Someone used my key. Will the provider refund me?
- Policies vary and you shouldn't count on it. Revoke the key straight away, check your usage page for the dates it was used, and contact the provider's support with those dates.
- Can I just restrict the key to my website's domain?
- Most AI and email providers don't offer that for secret keys, and a domain restriction can be faked by anyone sending requests outside a browser. Keep the key on the server.
Read next
Written by Obscura Security, an Australian practice that checks web apps for problems like this one. If you want a second pair of eyes on yours, email [email protected].