How to find API keys leaked into your JavaScript bundle
Anyone can read your site's JavaScript. Here is how to search everything your live site loads for secret keys, tell public keys from secret ones, and fix a leak.
· 5 min read
Everything your site sends to the browser can be read by anyone who visits it. That includes every JavaScript file, every source map and every environment variable your build tool decided to inline. If a secret key ended up in there, it is not hidden by minification or by being "only in the production build". It is public.
This is one of the most common ways AI-built apps get into trouble: the assistant needs to call OpenAI, Stripe or a database, the quickest working version puts the key in the frontend, and the app ships. This guide shows how to look for leaked keys on your live site, how to tell which keys are actually dangerous, and what to do if you find one.
Why keys end up in the frontend
There are three usual routes.
- The call was written client-side. "Add an AI summary button" turns into a
fetchtoapi.openai.comstraight from a React component, with the key next to it. - A public-prefix environment variable. Build tools inline variables with certain prefixes into the bundle on purpose:
NEXT_PUBLIC_in Next.js,VITE_in Vite,REACT_APP_in Create React App,EXPO_PUBLIC_in Expo. Naming a secretNEXT_PUBLIC_OPENAI_KEYto "make it work" publishes it. - A hard-coded fallback.
const key = process.env.KEY || "sk-…"left from testing.
The Vite documentation and the Next.js documentation both warn that prefixed variables end up in the client bundle. Your AI assistant does not always remember that.
Public keys and secret keys
Not every key in a bundle is a problem. Some are designed to be public, and panicking about them wastes time.
Usually fine in the frontend:
- Supabase anon key or
sb_publishable_…(safe only if Row Level Security is on — see how to check RLS in Supabase) - Stripe publishable key
pk_live_… - Firebase web config, Google Maps browser keys restricted by HTTP referrer
- Analytics and error-tracking IDs
Never in the frontend:
- Stripe secret
sk_live_…and restrictedrk_live_…keys - OpenAI
sk-proj-…and Anthropicsk-ant-…keys - Supabase
service_roleJWT orsb_secret_…key - AWS access keys starting with
AKIA - GitHub tokens (
ghp_…,github_pat_…), Slack tokens (xoxb-…), SendGrid keys (SG.…) - Anything that starts with
-----BEGIN PRIVATE KEY-----
The rule of thumb: if the provider calls it "secret", or if it lets someone spend your money or read your data, it belongs on a server.
Check it yourself
1. Look at what the live page actually loads
Your repository is not the source of truth here; the deployed site is. Open your site in Chrome, open DevTools, go to the Network tab, tick "Disable cache" and reload. Filter by JS. These are the files every visitor downloads.
2. Search all of them at once
In DevTools, press Cmd+Option+F (Mac) or Ctrl+Shift+F (Windows) to search across all loaded sources. Search, one at a time, for:
sk_live_
rk_live_
sk-proj-
sk-ant-
service_role
sb_secret_
AKIA
ghp_
github_pat_
xoxb-
BEGIN PRIVATE KEY
If you prefer the terminal, download the bundle files and search them:
mkdir bundle && cd bundle
curl -s https://your-site.com | grep -oE 'src="[^"]+\.js"' | cut -d'"' -f2 \
| while read -r path; do curl -s "https://your-site.com$path" -o "$(basename "$path")"; done
grep -nE 'sk_live_|rk_live_|sk-proj-|sk-ant-|sb_secret_|service_role|AKIA[0-9A-Z]{16}|ghp_|github_pat_' *.js
This only catches scripts linked directly from the HTML. Apps that load chunks lazily pull more files later, which is why the DevTools search after clicking around the app is the more complete check.
3. Check source maps
If your host serves .map files, the original source code is public too, including comments and fallbacks that minification removed. In the Network tab, look for requests ending in .js.map, or try opening one of your script URLs with .map added. If it loads, search it the same way.
4. Check what your build exposes
In the repository, list every public-prefix variable:
grep -rnoE '(NEXT_PUBLIC|VITE|REACT_APP|EXPO_PUBLIC)_[A-Z0-9_]+' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='.env*' .
Every name on that list is in your bundle. Any of them containing SECRET, PRIVATE, SERVICE or a provider's secret-key value is a leak.
Found one? Revoke first
Order matters here.
- Revoke or roll the key in the provider's dashboard right now. Removing it from the code does not help: it has already been served to every visitor, and old bundles stay in caches and archives.
- Check usage in the provider's dashboard for calls you did not make.
- Move the call to a server: an API route, a serverless function or a Supabase Edge Function. The browser calls your endpoint; your endpoint holds the key and calls the provider.
- Put the new key only in server-side environment variables, without a public prefix.
- Deploy and re-check the live bundle with the search above.
A prompt that works well with Lovable, Cursor or Claude Code:
A secret key for [provider] is exposed in the client bundle.
1. Remove every use of it from client-side code and from any
NEXT_PUBLIC_/VITE_/REACT_APP_ variable.
2. Create a server-side endpoint that performs the same call, reading
the key from a server-only environment variable named [NAME].
3. Make the client call that endpoint instead, and require the user
to be logged in if the feature is for logged-in users only.
4. List every file you changed.
I have already revoked the old key.
Keep it from coming back
Leaks are not a one-time event. The next feature, the next "quick fix", the next variable renamed to make a build pass can bring the key back, and nothing in your app will look different.
ScanYourSite does the search above for you. The free check downloads the scripts your live page loads and looks for secret keys from Stripe, OpenAI, Anthropic, AWS, GitHub, Slack, SendGrid, Supabase and others. Keys that are meant to be public, like the Supabase publishable key, are reported as information rather than alarms, and every key is shown redacted. On Pro, new scripts are checked as soon as a new version of your site appears, and you hear from us only when something changes.
Whether you use a tool or the checklist above, make the search part of every release, not just launch day.