Your API key is in your frontend. Here is how to find it and what to do
Anything shipped to a browser is readable by whoever opens developer tools, so a key in your frontend bundle is already public whether or not anyone has used it yet. Some keys are designed to be public and are fine. The test is not where the key sits, it is what the key can do.
There is a specific kind of email nobody wants, and it comes from your AI provider. It tells you that you spent four thousand dollars overnight.
It happens because a key ended up in the JavaScript your app sends to browsers, and bots scan for exactly that, continuously, at scale. This is the most common serious finding in AI-built apps and it is also the easiest one to check.
Why does the key end up there in the first place?
Because you asked for something reasonable and the model gave you the shortest thing that works. Both of those are the system behaving as designed.
"Add a chatbot to my app" has a two-line answer that calls the provider directly from the browser with your key in the request, and a twenty-line answer that adds a server route, moves the key into an environment variable, and proxies the call. Both produce a working chatbot in your preview. Only one of them keeps the key.
Nothing in the prompt distinguished them, so you got the short one. This is not a failure of the model so much as a question nobody asked it.
Is every key in the frontend a problem?
No, and this is where a lot of advice gets it wrong and frightens people for no reason. Some keys belong in the browser.
Some keys are designed to be public. A Supabase publishable key, a Stripe publishable key, a Google Maps browser key: these are meant to ship to browsers. They are not secrets, and finding one in your bundle is not a finding.
The test is not where the key is. It is what the key can do when someone else holds it.
| Key | In the browser? | Why |
|---|---|---|
| Supabase publishable / anon | Fine by design | Its power is bounded by your RLS policies and grants |
Stripe publishable (pk_) |
Fine by design | Can only tokenise, cannot charge or read |
| Google Maps browser key | Fine, with restrictions set | Restrict it by HTTP referrer or it bills you for strangers |
OpenAI / Anthropic (sk-) |
Never | Anyone who has it spends your money |
Stripe secret (sk_live_) |
Never | Charges, refunds, full customer data |
| Supabase service role / secret | Never | Bypasses RLS entirely, reads every row |
| Any database connection string | Never | It is the database |
One caveat on the "fine by design" column. A Supabase publishable key is only as safe as the rules behind it. If a table has RLS disabled, or has RLS on but still carries the grants Supabase handed out when it was created, that public key reads the table. The key is not the problem there. The table is.
How do I check my own app?
Do this against your deployed site, not your local dev server. What matters is what a stranger receives.
1. View source and search. Open your live app, view page source, and search for sk-, sk_live, secret, api_key, apikey, token and password.
2. Search the bundles, not just the HTML. Most of your code is in a .js file the page loads, not in the page. Open developer tools, go to the Network tab, reload, and search across the loaded scripts. In Chrome, Ctrl+Shift+F inside developer tools searches every loaded resource at once. That is the one that finds things.
3. Check the Network tab for requests going somewhere other than your own domain. A call from the browser straight to api.openai.com carries whatever authorises it, and that request is visible to the person making it.
4. Check your git history. A key removed from the current code is still in the commit that added it, and a public repository publishes every commit. git log -p | grep -i "sk-" will tell you.
I found one. Now what?
In this order, and the order matters more than any individual step. Doing the third one first wastes the twenty minutes that count.
Rotate it first. Not after you fix the code. The moment a key is public, the only thing that helps is that key no longer working. Go to the provider's dashboard and revoke it. Everything else can wait twenty minutes.
Check what was done with it. Most providers show usage. OpenAI and Anthropic show spend by day. AWS has CloudTrail. Stripe has an events log. Look for anything you do not recognise, and look before the memory fades.
Then move it server-side. The pattern is always the same. Your browser calls a route on your own server, your server holds the key in an environment variable and calls the provider, and the answer comes back. The key never leaves your infrastructure.
Every platform in this space supports this. On Bolt and Lovable it is a Supabase Edge Function. On v0 and Next.js it is a route handler under app/api/. If you are pasting this into your builder, describe the destination and not just the problem:
Move my OpenAI key out of the frontend. Create a server-side route that holds the key in an environment variable, calls the provider, and returns only the response. Update the frontend to call my route instead of calling the provider directly. Show me every place the key is currently referenced.
Delete it from git history if the repository is public. Rotating handles the risk. Removing the commit handles the embarrassment, and it is genuinely awkward, so rotate first and treat this as cleanup.
How do I stop it happening again?
The habit that works is boring: any time you ask an AI builder to integrate a paid service, add one sentence to the prompt saying the key must stay server-side. It costs you five words and it changes which of the two answers you get.
Beyond that, the reason this class of bug survives is that nothing in your workflow reads the shipped bundle and asks whether anything in it should be secret. Your app works, your tests pass, your preview looks right.
That is the gap Sentrint fills. It reads your whole repository, finds the credentials that should not be there, and gives you a fix you can paste back into the tool that wrote them. A code scan, not a pentest. The first one is free and needs no card.
Do the Ctrl+Shift+F search today though. It takes two minutes and it is the highest-value two minutes in this article.