API key exposed in your frontend? Here's what to do
If an API key is in your frontend code, it's public, no matter how it got there or how hard it looks to find. The good news is the fix is well understood.
The problem: "hidden in the bundle" is not hidden
Modern frontends get compiled into JavaScript files that every visitor downloads. Minification makes the code ugly, but it doesn't hide anything. Any string in that code, including an API key, can be found with the browser's built-in developer tools in under a minute.
Build tools are explicit about this. Vite's environment variable docs say variables prefixed with VITE_ are exposed in client-side code and shouldn't contain secrets. Next.js's docs explain that NEXT_PUBLIC_ variables are inlined into the JavaScript sent to the browser. AI builders lean on these prefixes because they make things "just work", which is exactly how secrets slip through.
What can someone do with a leaked key? That depends on the key:
- Stripe secret key (
sk_live_): act on your Stripe account, such as reading customers or issuing refunds. Stripe's docs say to keep secret keys on your server. (Thepk_live_publishable key is meant to be public.) - OpenAI, Anthropic and other AI keys: run requests on your account and your bill. OpenAI's production best practices advise against exposing keys in browsers or apps.
- AWS access keys: anything the attached permissions allow, which is sometimes a lot.
- Supabase service_role /
sb_secret_: full database access that bypasses Row Level Security. See this guide. - Google API keys (
AIza...): often meant for the browser (e.g. Maps), but they should be restricted to your domain and to the specific APIs you use, or someone else can use your quota.
Not every key in a frontend is a leak. Publishable keys (Stripe pk_, Supabase anon/sb_publishable_, most analytics IDs) are designed to be public. The skill is telling the two apart.
How to check your app yourself
In the browser
- Open your live site (the deployed version, not localhost) and open developer tools.
- Go to Sources and search all files (Ctrl/Cmd+Shift+F). Try:
sk_live_,rk_live_,sk-,sk-ant-,AKIA,sb_secret_,service_role,secret,apiKey,Bearer. - In the Network tab, reload and click through your app. Look at request headers going to third parties. If your browser is sending
Authorization: Bearer sk-...straight to an AI provider, that key is public.
In your code
# every env var that your bundler will publish
grep -rnE "(VITE_|NEXT_PUBLIC_|REACT_APP_|EXPO_PUBLIC_)[A-Z_]*(SECRET|KEY|TOKEN|PASSWORD)" \
--include=*.ts --include=*.tsx --include=*.js --include=*.jsx --include=.env* .
Each match deserves a look: is this value really safe for the public?
In your git history
If a key was ever committed, it's in your history even after you delete it. If your repo is on GitHub, secret scanning can alert you to many known key formats.
With the free scan
ShipWatch fetches your page and up to 25 of its JavaScript files and checks them for Stripe live keys, OpenAI and Anthropic keys, AWS access key IDs, Google API keys and Supabase secret keys. It shows a masked preview so the report itself doesn't leak the key again.
How to fix it
The fix always has the same shape: the browser calls your server, and your server calls the API with the key. The key lives in your host's secret storage and never reaches the bundle.
There is a secret API key in my frontend code. Move every call that uses it into a server-side endpoint (an API route, serverless function or Supabase Edge Function). Store the key as a server-only environment variable without any VITE_, NEXT_PUBLIC_ or REACT_APP_ prefix. The frontend should call my endpoint, and the endpoint should check the user is signed in before calling the third-party API. Show me every file you changed.
My app calls the OpenAI (or Anthropic) API directly from the browser. Create a server endpoint that proxies these requests: it should require a signed-in user, limit how many requests each user can make per minute, cap the maximum tokens, and only allow the specific model and prompt format my app uses, so the endpoint can't be used as a free general-purpose proxy.
List every environment variable my app uses and label each one "public (safe in browser)" or "secret (server only)". Rename any secret that has a public prefix, and update the code so secrets are only read in server-side code.
Then rotate. This step isn't optional.
Changing the code doesn't revoke the old key. Anyone who already copied it can keep using it. In each provider's dashboard, create a new key, put it in your server's environment, deploy, then revoke the old one. For Google keys you intend to keep in the browser, add HTTP referrer and API restrictions instead.
Check for misuse
Look at the provider's usage and billing pages for activity you don't recognize, and set usage limits or billing alerts where the provider offers them.
Make it hard to happen again
- Add
.envand.env.*to.gitignore, and keep a.env.examplewith blank values instead. - Before accepting an AI change that adds an integration, ask: "Does any secret end up in client code?"
- Rescan your live site after each deploy, since that's the version attackers see.