OpenAI API key exposed in your frontend? What to do now
Your OpenAI key ended up in client-side code. Revoke it, check usage, move the call behind a server endpoint that can't be abused — step by step, with a prompt.
· 6 min read
You built an AI feature, it works, and then you notice it: the OpenAI key is sitting in your frontend code. Maybe a VITE_OPENAI_API_KEY variable, maybe a string inside a React component, maybe dangerouslyAllowBrowser: true in the SDK setup. Or you got an email about unusual usage and traced it back here.
Either way, the key is public. Every visitor to your site downloaded it, and anyone who opened DevTools could copy it. This guide is the order of operations: stop the damage, confirm what happened, and rebuild the feature so the key never reaches the browser again — including the part most quick fixes skip.
Why this happens so often with AI-built apps
Calling OpenAI directly from the browser is the shortest path to a working demo, and AI builders optimise for working demos. Three patterns show up again and again:
- A public-prefix environment variable. Vite inlines every
VITE_variable into the bundle, Next.js does the same withNEXT_PUBLIC_. Naming the keyVITE_OPENAI_API_KEYmakes the build pass and publishes the key in the same step. - The SDK guard was switched off. The official
openaipackage for JavaScript refuses to run in a browser by default. Its README says browser support is disabled to avoid exposing your secret credentials, and you have to setdangerouslyAllowBrowser: trueto override that. When an assistant hits that error, flipping the flag is the "fix" it sometimes reaches for. - A plain
fetchtoapi.openai.comwith anAuthorization: Bearer sk-…header, written straight into a component.
OpenAI's own guidance is blunt: in its API key safety best practices it tells you never to deploy your key in client-side environments like browsers or mobile apps, because anyone can take it and make requests on your behalf. Minification, obfuscation or "it's only in the production build" don't change that.
Check it yourself
Before you fix anything, confirm what is actually live. The deployed site is what matters, not your repository.
1. Search what the browser loads
Open your site, open DevTools, go to the Network tab, tick "Disable cache" and reload. Click around the pages that use the AI feature so lazily loaded scripts arrive too. Then press Cmd+Option+F (Mac) or Ctrl+Shift+F (Windows) and search for:
sk-proj-
sk-svcacct-
sk-admin-
api.openai.com
dangerouslyAllowBrowser
A hit on sk-proj- or sk-svcacct- is a project or service-account key. A hit on sk-admin- is worse: admin keys manage your organisation, not just completions. A hit on api.openai.com without a key means the browser is calling OpenAI directly, so look at the request headers in the Network tab when you use the feature.
2. Watch a real request
Use the AI feature once with the Network tab open. Click the request it makes and look at two things:
- Where it goes. If the URL is
api.openai.com, the browser is talking to OpenAI directly and must be holding the key. - The
Authorizationheader. If it containsBearer sk-…, that is your key, in plain text, in every user's browser.
3. Find the source in your code
grep -rnE 'sk-(proj|svcacct|admin)-|OPENAI_API_KEY|dangerouslyAllowBrowser|api\.openai\.com' \
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='.env*' .
Every match in a file that runs in the browser — components, hooks, anything under src/ in a Vite app — and every VITE_/NEXT_PUBLIC_ variable holding the key is part of the leak. For a broader sweep across all secret types, see how to find API keys in your JavaScript bundle.
Stop the damage first
Do these in order, before touching the code.
- Revoke the key. Go to the API keys page in the OpenAI dashboard and delete the exposed key. Removing it from your code does nothing for copies already taken: the old bundle has been served to every visitor and may sit in caches and archives.
- Create a new key in the same project, with an expiry date if your organisation allows it, and do not paste it anywhere yet.
- Check usage. Open the usage page in the OpenAI dashboard and look at the period since the key went live. Spikes at odd hours, models your app never calls, or volume far above your traffic are signs someone else used it.
- If you see misuse, contact OpenAI support and keep the dates and amounts. Don't wait for the invoice.
- If the key was in a public repository too, remember that removing it in a new commit leaves it in Git history. Revoking is what makes it harmless.
Your AI feature is now broken in production. That's expected — the next step brings it back properly.
Fix it: a server endpoint that can't be abused
The standard fix is to move the OpenAI call to a server: an API route, a serverless function, or a Supabase Edge Function. The browser calls your endpoint, the endpoint holds the key and calls OpenAI.
The part people skip: an endpoint that forwards anything to OpenAI is just a new leak. If it accepts any prompt, any model and any token limit from anyone, a stranger doesn't need your key — they use your endpoint and you still pay. So the endpoint should:
- Require a logged-in user if the feature is for users, and reject anonymous requests.
- Fix the model and
max_tokenson the server. Don't read them from the request body. - Build the prompt on the server. The client sends the user's input; the instructions and system prompt live server-side.
- Limit requests per user, even crudely: a counter per user per day is better than nothing.
- Read the key from a server-only variable with no public prefix, e.g.
OPENAI_API_KEY.
If your app runs on Supabase (most Lovable and Bolt apps do), an Edge Function is the natural home. Secrets are set with supabase secrets set OPENAI_API_KEY=... or in the dashboard, and read inside the function with Deno.env.get('OPENAI_API_KEY'), as described in the Supabase Edge Function secrets docs. The function can check the user's JWT before doing anything.
A prompt for Lovable, Cursor or Claude Code:
My OpenAI API key was exposed in client-side code. I have already
revoked it. Fix the AI feature properly:
1. Remove every OpenAI call, the openai SDK setup, any
dangerouslyAllowBrowser option and any VITE_/NEXT_PUBLIC_ variable
holding the key from client-side code.
2. Create a server-side endpoint (a Supabase Edge Function if this
project uses Supabase, otherwise an API route) that calls OpenAI
with the key read from a server-only env variable OPENAI_API_KEY.
3. The endpoint must: reject requests without a logged-in user,
hard-code the model and max_tokens, build the system prompt on
the server, accept only the user's input text (with a length
limit), and limit each user to [N] requests per day.
4. Update the client to call this endpoint with the user's session.
5. List every file you changed and tell me where to put the new key.
After it's done, put the new key only in the server-side secret store, deploy, and run the three checks above again. The search for sk-proj- should find nothing, and the feature's request should go to your own domain or your Supabase project, not to api.openai.com.
Keep it from coming back
The uncomfortable thing about this leak is how easily it returns. A later prompt like "the AI button stopped working, fix it" can lead the assistant straight back to the browser call, and nothing on the site looks different.
Two habits help: never give a secret a VITE_ or NEXT_PUBLIC_ name, and repeat the DevTools search after any change to the AI feature. If the key was in Supabase-adjacent code, it is also worth reading anon vs service_role keys — the same rule applies there.
ScanYourSite automates the search. The free check downloads the page and the scripts it loads and looks for secret-key formats, including OpenAI project, service-account and admin keys, Anthropic keys, Stripe secrets and more. Keys are shown redacted, with the file they're in. The free plan re-checks one site weekly; on Pro, every new deploy is checked within minutes, so a key that slips back in is caught the day it ships, not on the next invoice.