Everything your frontend needs to run is downloaded by every visitor. That includes any key your code uses directly. Escape's October 2025 study of 5,600+ public vibe-coded apps found 400+ exposed secrets, and VibeEval's August 2026 scan found that 1,332 of 30,998 live apps shipped a secret, roughly 1 in 23.
Not every key in a bundle is a problem, though. The question is which kind of key it is.
Short answer: publishable keys (Supabase anon, Firebase web config, Stripe
pk_) are designed to be public and are protected by rules on the provider's side. Secret keys (Stripesk_, OpenAI, Anthropic, Supabase service role, database URLs with a password) must never reach the browser. If one did, rotate it first and ask questions later.
Keys that are designed to be public
These identify your project. They are safe in the browser as long as the rules behind them are right:
| Key | Looks like | What actually protects you |
|---|---|---|
| Supabase public key | sb_publishable_… or an anon JWT |
Row Level Security and your policies |
| Firebase web config | apiKey: "AIza…" inside firebaseConfig |
Firebase Security Rules for Firestore, Realtime Database and Storage |
| Stripe publishable key | pk_live_… / pk_test_… |
Stripe only lets it create tokens and start checkout |
| Mapbox public token | pk.… |
URL restrictions on the token |
| Algolia search key | search-only API key | Read-only by design; never ship the admin key |
| Google Maps browser key | AIza… |
HTTP referrer and API restrictions in Google Cloud |
A public key with bad rules behind it is still a leak. It is the rules, not the key, that need fixing.
Keys that must never be in frontend code
| Key | Looks like | What someone can do with it |
|---|---|---|
| Stripe secret or restricted key | sk_live_…, rk_live_… |
Read customers, issue refunds, create charges |
| OpenAI key | sk-proj-…, sk-… |
Run up your bill on your account |
| Anthropic key | sk-ant-… |
Same: usage billed to you |
| Supabase secret / service role | sb_secret_… or a service_role JWT |
Bypass every RLS policy: read and change all data |
| Database connection string | postgres://user:password@… |
Connect to your database directly |
| GitHub token | ghp_…, github_pat_… |
Read or push to your repositories |
| Email and SMS providers | SendGrid SG.…, Twilio auth token, Mailgun |
Send spam or phishing from your domain |
| Signing secrets | JWT_SECRET, NEXTAUTH_SECRET |
Forge sessions and log in as anyone |
How secrets end up in the bundle
The most common route is an environment variable with a public prefix. Build tools inline these into the JavaScript on purpose:
- Vite: anything starting with
VITE_ - Next.js: anything starting with
NEXT_PUBLIC_ - Create React App: anything starting with
REACT_APP_ - Expo: anything starting with
EXPO_PUBLIC_
VITE_OPENAI_API_KEY is not hidden because it came from a .env file. It is compiled into your site. The prefix is exactly the instruction "put this in the browser".
The second route is an AI assistant calling a paid API directly from a React component, because that is the shortest path to a working demo. It works, and every visitor can copy the key from the network tab.
How to check what your app ships
- Open your live site, then your browser's developer tools.
- In Sources (Chrome) or Debugger (Firefox), search across all files (
Ctrl/Cmd + Shift + F) forsk_,sk-,service_role,sb_secret,postgres://,SECRETandKEY. - In the Network tab, watch the requests your app makes. A request that goes straight from the browser to
api.openai.comorapi.stripe.comwith a secret in its headers is the problem, visible to anyone. - Locally, search your production build output (
dist/,.next/,build/) for the same strings.
Or run a PreflightX scan: it fetches your HTML and JavaScript bundles like a visitor would and reports each credential with its type, file and line, showing only the last four characters.
If a secret key is exposed: do this in order
- Rotate it now. Create a new key in the provider's dashboard and revoke the old one. Deleting it from your code is not enough: old bundles stay cached, and your git history still has it.
- Check what it was used for. Look at the provider's usage, logs and billing since the key went live. Set a spending limit where the provider offers one.
- Move the call to the server. Put the secret in a server-only environment variable (no public prefix) and call the provider from a backend route, a Supabase Edge Function, or a serverless function. The browser talks to your function; only your function knows the key.
- Add authorization to that function. A server route that anyone can call with any input is just a slower way to leak your key's budget. Require a signed-in user and validate what they send.
- Scan again after you deploy, and confirm the key is gone from the live bundle.
Frequently asked questions
Is it safe to put my Firebase API key in the frontend?
Yes. The Firebase web apiKey identifies your project. What protects your data is Firebase Security Rules, so check those instead.
My key is only in a .env file. Is that enough?
Only if it has no public prefix and is used exclusively by server-side code. A VITE_ or NEXT_PUBLIC_ variable ends up in the browser no matter which file it came from.
The key was only exposed for a few hours. Do I still need to rotate it? Yes. Bots scan public sites and repositories for keys continuously. Treat any exposed secret as compromised.
Check what your app exposes right now
PreflightX runs a read-only scan of your live app's public surface: exposed keys in your JavaScript, readable Supabase tables, open storage buckets, deployed .env and .git files, source maps and missing security headers. Every finding is free to see.