API key exposed in your frontend? How to tell and fix it
How to check whether a secret API key ships in your app's browser code, which keys are meant to be public, and how to move the secret ones to a server.
Leer en español →
If a secret API key is in your frontend code, anyone who opens your site can read it, and hiding it in a file, an environment variable or minified JavaScript does not change that. The fix is to revoke the key, create a new one, and move every call that needs it to a server (an Edge Function, a serverless function or your backend) that the browser talks to instead. Public keys, such as a Stripe publishable key or a Supabase publishable key, are designed to be visible and are not the problem.
This guide shows how to check your own app from the outside, how to tell a public key from a secret one, what the fix looks like step by step, and how to stop it from happening again with the next AI prompt.
How do I check if my app exposes an API key?
You can do this in ten minutes with nothing but a browser. Do it on the live site, not the preview, because that is what visitors get.
- Open your app, right-click and choose Inspect, then open the Sources tab (in Firefox, Debugger).
- Press
Ctrl + Shift + F(orCmd + Option + Fon a Mac) to search across every loaded file. - Search for the prefixes that secret keys usually start with:
sk_live,sk_test,sk-,sb_secret_,service_role,secret,Bearer. - Switch to the Network tab, use the app normally (log in, run the main feature) and click on requests. Look at the request headers: if your browser sends an
Authorizationheader straight to a third-party API such as OpenAI or SendGrid, the key is in the browser. - Check the source code too. In a Vite project, every variable starting with
VITE_is copied into the browser bundle. In Next.js, the same goes forNEXT_PUBLIC_. If a secret lives in one of those, it is public.
If you find a match, assume it has already been copied. Automated scrapers look for exactly these strings.
Which keys are safe in the browser and which are not?
Not every key you find is a leak. Some services split their keys into a public half and a secret half on purpose.
| Key | Safe in browser? | Why |
|---|---|---|
Stripe publishable key (pk_live_…) | Yes | It can only start payments, not read or refund them |
Stripe secret or restricted key (sk_…, rk_…) | No | It can read customers, issue refunds, move money |
Supabase publishable or legacy anon key | Yes, with Row Level Security on | It only gets what your policies allow |
Supabase secret or service_role key | No | It bypasses Row Level Security entirely |
| Firebase web config | Yes, with security rules | Access is decided by your rules |
| Google Maps browser key | Yes, if restricted | Lock it to your domains and to the APIs you use |
| OpenAI, Anthropic, Gemini and other AI keys | No | Anyone can spend your credit with them |
| Email, SMS and database passwords | No | Full access to send or read on your behalf |
A "public" key is only safe if the thing behind it is locked. A Supabase publishable key with Row Level Security switched off is as dangerous as a secret key, because the database answers anyone. Our guide on whether the Supabase anon key is safe to expose goes into that case.
Why do AI builders put secret keys in the frontend?
Because it is the shortest path to a working preview. When you ask for "add AI summaries" or "send a welcome email", the quickest code calls the provider directly from the React component with the key pasted next to it. It works on the first try, so nothing tells you it is wrong.
Two other patterns cause the same result:
- A secret was added as
VITE_OPENAI_KEYorNEXT_PUBLIC_STRIPE_SECRETbecause the code could not read it without the prefix. - A key was pasted into a prompt or a config file and ended up committed to GitHub, where it stays in the history even after you delete the line.
How do I fix an exposed API key?
Order matters here. Moving the code first and revoking later leaves the old key working in the meantime.
1. Revoke and replace the key
Go to the provider's dashboard, create a new key, and revoke or delete the exposed one. Removing the key from your code does nothing on its own: the copy someone already took still works until the provider disables it. If the key is used by something you cannot update right away, create the new one first, switch over, then revoke the old one the same day.
2. Check what the key was used for
Look at the provider's usage or logs page for the days the key was public. Unusual spikes, requests from countries you don't serve, or charges you don't recognise mean it was used. For AI keys, our guide on what to do when an OpenAI key leaks covers the billing side.
3. Move the call to a server
Create a small server-side function that does the one thing the frontend needs: "summarise this text", "send this email", "create a checkout session". The browser calls your function; your function holds the key and calls the provider.
Browser -> /functions/summarise (checks the user is signed in)
|
v
AI provider (key read from a server-side secret)
The key goes into the function's secret store (Supabase Edge Function secrets, Vercel or Netlify environment variables without the public prefix) and never into a file the browser loads.
4. Protect the new function
A server function that anyone can call is a free proxy to your paid account. Make it check that the request comes from a signed-in user, validate the input size, and limit how often each user can call it. Our guide on rate limiting AI features shows how.
5. Clean up the history
If the key was ever committed to Git, removing it from the latest version is not enough. Rotating it (step 1) is what actually protects you; rewriting history is optional tidy-up. GitHub's guide to remediating a leaked secret explains both.
How do I confirm the fix worked?
Repeat the check from the first section on the live site after redeploying. Search the Sources tab again for the old prefixes, watch the Network tab to confirm the browser now calls your own function rather than the provider, and test that the old key is rejected by the provider. Finally, call your function while signed out: it should refuse.
How do I stop it from happening again?
- Keep a short list of which variables are public and which are secret, and never give a secret a
VITE_orNEXT_PUBLIC_prefix. - Add a line to your AI tool's project rules: "Never call paid third-party APIs from the browser; create a server function."
- Turn on secret scanning and push protection in GitHub so a commit containing a known key format is flagged.
- Set spending limits and alerts with each provider, so a leak shows up as an alert rather than a bill.
Our security checklist for AI-built apps puts this check alongside the others worth doing before launch.
Quick answers
Is a key in an environment variable automatically safe? No. It depends on where the variable is used. If the frontend build reads it, it ends up in the browser bundle.
Can I hide the key by obfuscating or encrypting it in JavaScript? No. The browser has to decrypt it to use it, so anyone can do the same.
Is the Stripe publishable key a leak? No. It is meant to be public. Only secret and restricted keys must stay on the server.
Do I need to delete my GitHub repository? No. Revoke the key with the provider. Deleting or rewriting the repository does not disable a copy someone already made.
Want someone to check it for you?
The free health check looks at what your live app exposes from the outside, including keys in the browser bundle. If you want a person to go through the code, move the calls to a server and review your database rules, our Supabase security review and maintenance plans cover that work, or you can tell us what you found.


