Exposed API Key in Frontend
Anything in your frontend JavaScript is public. Minifying doesn't hide it, and neither do obfuscation or an environment variable that gets inlined at build time. A visitor can open DevTools, or a bot can download your bundle, and read every string in it. So the real question isn't how to hide an API key in the frontend. It's which keys are allowed to be there, and what to do with the ones that aren't.
The Pro Logica AI App Security Check scans your live JavaScript for exposed keys and masks anything it finds. Run the free scan.
Safe to expose vs. rotate now
Designed to be public (no rotation needed, but configure them properly):
- Supabase anon / publishable key. It's only safe when Row Level Security protects every private table. See Supabase RLS not enabled.
- Firebase web config and Google browser keys (
AIza…). Firebase's docs say these keys aren't secret. Restrict them in Google Cloud Console by HTTP referrer and by API so nobody can run up charges on paid APIs like Maps, and rely on Security Rules for your data. - Stripe publishable keys (
pk_live_…), analytics IDs, and public map tokens that have URL restrictions.
Never in the browser. If one is there, assume it's already been copied:
- Stripe secret (
sk_live_…) or restricted (rk_live_…) keys - AWS access keys (
AKIA…) and secret keys - OpenAI (
sk-…) and Anthropic (sk-ant-…) keys - GitHub (
ghp_…,gho_…) and Slack (xox…) tokens - SendGrid keys (
SG.…), Google OAuth client secrets (GOCSPX-…) - Private key blocks (
-----BEGIN … PRIVATE KEY-----) - Supabase
service_roleor secret keys, which bypass all RLS
Those are the patterns our scanner looks for in your homepage and up to eight of your own JS files. We also flag generic api_key = "…", secret: "…" or token = "…" assignments for you to review. Some turn out to be harmless, and some are real.
How keys end up in the bundle
- Public env prefixes. Vite inlines anything prefixed
VITE_. Next.js inlines anything prefixedNEXT_PUBLIC_. AI builders sometimes add the prefix just to get past a "variable is undefined" error. - Calling a paid API straight from a component, such as an OpenAI chat call made inside a React
onClick. - Hard-coded fallbacks like
const key = process.env.KEY || "sk_live_…". - Pasting a key into a builder's chat and letting it write the key into source. Lovable now detects keys pasted into chat and steers you to Secrets. Not every tool does that.
The fix, in order
1. Rotate the key at the provider first. Deleting it from your code doesn't help, because old bundles, caches and anyone who already copied it still have it. Create a new key, update your server config, then revoke the old one. Check the provider's usage logs for activity you don't recognize.
2. Move the secret to a server-only variable with no VITE_ or NEXT_PUBLIC_ prefix. Put it in your platform's secret store: Lovable Secrets, Replit Secrets, Vercel or Netlify environment variables, or Supabase Edge Function secrets.
3. Add a small server route (an API route, server action, edge function or Express handler) that holds the key and calls the third-party API. The browser calls your route instead. This is the only reliable way to "hide" a key: keep it off the client entirely.
4. Protect that route. Require a signed-in user where it makes sense, validate input, and add a rate limit. Otherwise you've just built a free proxy for your key.
5. Stop it from happening again. Add a CI step or pre-commit hook that fails when sk_live_, AKIA or a service_role JWT appears in src/ or public/. Netlify (on paid plans) and GitHub both offer secret scanning that can catch keys before or after a push.
6. Rescan your live URL to confirm the key is gone from what you actually ship.
What we saw in public apps
In our passive check of 18 public showcase apps built with Lovable or Bolt, 1 of 18 had a value in its client JavaScript that looked like a secret string, and 7 of 18 had a Supabase anon key (which is expected). We kept only the kind of match, never the value, and we didn't try to use or verify any key. No app in the sample had a critical-class finding under those passive checks.
Related
- Supabase RLS not enabled
- Exposed .env file
- Source maps exposed in production. Maps can leak keys from comments, too.
- By builder: Lovable · Bolt.new · Replit · Cursor · v0
- More help: Pro Logica cybersecurity services · Run the free check on your app
FAQ
Can I hide an API key in the frontend with obfuscation or encryption?
No. If the browser can use the key, the browser has to be able to read it, and so can anyone else. Put the key on a server and call your own route.
Is a Firebase or Google AIza key in my code a leak?
Not by itself. Those keys are meant to identify your project. Restrict them by referrer and API, and secure your data with Security Rules. Our scan marks them low severity for that reason.
I removed the key from my code. Am I safe?
Not until you rotate it. Assume anyone could have copied it while it was public.
Do you store the keys you find?
No. We store the kind of key and a masked form, like sk_…abc (len 107). The full value is never saved or shown.
What does the full report add?
Masked evidence for each finding, the exact fix steps, and a copy-paste prompt for Lovable, Bolt, Replit, Cursor or v0 that moves the key server-side and adds the CI guard.
See what your bundle gives away. Run the free AI app security check. Questions? Call 949-205-7333.
By Pro Logica