Exposed Secrets and API Keys in Frontend Code: The Complete Guide
An exposed API key in your frontend isn't always an emergency. Learn which keys are public by design, which are real leaks, and how to find and rotate the dangerous ones.
You opened your app in the browser, pressed a couple of keys, and there it was — an API key sitting in plain text inside your own code. Maybe a scanner flagged it. Maybe a stranger on Reddit told you anyone can read it. Now you're wondering if you have to take the whole thing down.
Take a breath. An exposed API key in your frontend is sometimes a five-alarm fire and sometimes completely fine — and the difference is not subtle once you know what to look for. Some keys are built to live in the browser where everyone can see them. Others should never get within a mile of it. Confusing the two is how founders either panic over nothing or sleep through a real breach.
This is the hub for everything about exposed secrets in AI-built apps. We'll cover which keys are public by design, which ones end your week if they leak, how secrets sneak into a frontend bundle in the first place, what it actually costs when the dangerous kind gets out, and exactly how to find and fix yours.
⚡ TL;DR
- Some keys are public by design and meant to ship in the browser: Stripe
pk_live, the Supabaseanonkey, and the Firebase web config. Seeing these in your frontend is normal — not a leak.- The real emergencies are secret keys that should never reach the browser: Stripe
sk_live, the Supabaseservice_rolekey, and OpenAI or Anthropic API keys. If one of these is exposed, rotate it now.- Secrets end up in frontend code three main ways — the AI hardcodes them, a build tool bundles a server environment variable into client code, or a published source map leaks the original code.
- You can check your own app in a few minutes, and a real leak is fixable: rotate the key, then audit what was touched.
Public by design vs secret: the one distinction that matters
Almost every "exposed key" scare comes down to one question: is this key supposed to be public, or not?
Modern apps use two kinds of keys, and they live in different worlds.
A publishable or public key is meant to be seen. The whole system is designed around the assumption that anyone can read it. It identifies your project to a service, but it can't do anything dangerous on its own. Shipping it in your browser code is the intended behavior, not a mistake. Three you'll run into constantly:
- Stripe
pk_live_…— the publishable key. It's how Stripe's checkout form knows which account to attach a payment to. It cannot move money out, issue refunds, or read your customer list. It is supposed to be in your frontend. - Supabase
anonkey — the anonymous public key. Supabase apps talk to the database directly from the browser, so this key has to ship. Your security comes from Row Level Security (RLS = the database rules that decide which rows each user can see), not from hiding this key. - Firebase web config (
apiKey) — despite the name, this is not a secret. It's an identifier that tells the Firebase SDK which project to connect to. Google publishes it in their own docs. Your security comes from Firebase Security Rules, not from hiding the config.
A secret key is the opposite. It carries real authority — it can charge cards, read every row in your database, or spend your money on someone else's AI bill. These are meant to live only on a server you control, where users can't see them. The dangerous trio:
- Stripe
sk_live_…— the secret key. Full access to your Stripe account. It can pull your entire customer list, issue refunds, and create charges. If this is in your frontend, that is an emergency. - Supabase
service_rolekey — the admin key. It bypasses RLS entirely and can read and write every row in every table. It belongs only in server-side code or an edge function, never the browser. - OpenAI / Anthropic API keys (and any LLM provider) — these are secret and metered. Whoever holds the key can run requests that bill to your account, and can read data flowing through it. We dig into the cost in an exposed OpenAI or Anthropic key is someone else's bill on your card.
Here's the simple tell for the most common case: a pk_ prefix is publishable (public, fine). An sk_ prefix is secret (emergency). That single letter is the whole difference, and we walk through it in detail in did I leak my Stripe key? pk_live vs sk_live explained.
🐺 Not a real problem
Finding a Stripe
pk_live, a Supabaseanonkey, or a Firebase web config in your browser code is not a leak. These are public by design. If a scanner told you to rotate one of them, it's crying wolf. Save your panic for thesk_live,service_role, and LLM keys.
How secrets end up in a frontend bundle
If the secret keys are supposed to stay on the server, how do they keep ending up in browser code? Three ways, and AI builders make all three easy to do by accident.
1. The AI hardcodes the key directly. When you ask an AI builder to "connect to OpenAI and summarize this text," the fastest path to a working demo is to paste the key straight into the code that runs the feature. In a preview, it works perfectly. But if that code runs in the browser, the key ships to every visitor. The AI optimized for "it works," not "it's secure" — that's the whole pattern behind most of these gaps.
2. A build tool bundles a server environment variable into client code. Frameworks like Vite, Next.js, and Create React App have rules about which environment variables get exposed to the browser. Anything prefixed VITE_, NEXT_PUBLIC_, or REACT_APP_ is deliberately baked into the frontend bundle. Put a secret key behind one of those prefixes — VITE_OPENAI_KEY — and the build tool will faithfully publish it to every user. It's not a bug. The tool did exactly what the prefix told it to.
3. A published source map leaks your original code. Source maps are files that map your minified production bundle back to the readable code you wrote, so error tools can show useful stack traces. Helpful in development — but if you ship them to production, anyone can download them and read your original source, comments and all, including any secret you left in there. This is sneaky because the key might not be visible in the minified bundle but is sitting in plain sight in the source map. We cover it in your source maps are publishing your source code.
The common thread: in all three cases the secret reaches the browser, and anything in the browser is readable by anyone. Minification is not encryption. "Hidden" in a 2MB JavaScript file is not hidden.
What it actually costs when a secret leaks
It's tempting to think a leaked key in a small app is a small problem. The reality is that a single exposed secret can hand over far more than the one feature it powers.
The clearest recent example is Moltbook, an AI-powered social network exposed by Wiz researchers in January 2026. The root cause was missing RLS on a Supabase backend — the same default gap that haunts AI-built apps. Through it, attackers could pull roughly 1.5 million API keys and around 35,000 user emails. Worse: some of the app's private direct messages contained plaintext OpenAI keys users had pasted in. Those are live, metered, secret keys — whoever scraped them could run AI requests on someone else's account (someone else's bill on your card) and reach whatever data those accounts could touch.
Two lessons sit inside that one incident: a leak rarely stays contained — one missing rule exposed keys and emails and DMs at once — and secret keys are valuable enough that people actively hunt for them, so exposed keys get scraped and abused fast.
This isn't a one-off. When the security firm Escape.tech scanned 5,600 vibe-coded apps, they found over 2,000 vulnerabilities, more than 400 exposed secrets, and 175 instances of exposed personal information. Exposed secrets are among the most common findings in AI-built apps, not a rare edge case.
The good news, which we'll keep coming back to: a real leak is fixable. Rotating a key takes minutes. The damage comes from not knowing a secret is out there, not from the fix.
How attackers find exposed keys
You don't need to be a hacker to understand how this works, and understanding it makes the risk concrete.
Your app's frontend code is public. It has to be — the browser downloads and runs it. So anyone can:
- Open the browser's developer tools and read the JavaScript your site shipped. No special access required; it's the same code every visitor receives.
- Search the bundle for key patterns. Secrets follow recognizable formats —
sk_live_,sk-,sk-ant-,service_roletokens are JSON Web Tokens with a tell-tale shape. A simple text search across your bundle surfaces them in seconds. - Download your source maps, if you published them, and read your original code directly — far easier than picking through minified output.
- Run automated scanners that crawl thousands of sites and flag exposed secrets at scale. This is largely how researchers find these issues in the first place, and attackers run the same playbook.
We break down steps 1 and 2 in how attackers extract secrets from JS bundles, and the source-map angle in your source maps are publishing your source code. The takeaway for now: if a secret is in your frontend, assume it can be found, because finding it is mechanical and cheap.
How to check your own app
You can do a first pass yourself in about ten minutes. You're looking specifically for the secret keys — the public ones being there is expected.
- Open your live app, then open developer tools (right-click → Inspect, or F12). Go to the Sources or Network tab so you can see the JavaScript files your app loads.
- Search the loaded code for secret prefixes. Use the search box (often Ctrl/Cmd-Shift-F in dev tools) and look for:
sk_live_(Stripe secret),sk-andsk-ant-(OpenAI and Anthropic), andservice_role(Supabase admin). Finding any of these in browser-delivered code is a real problem. - Ignore the public keys. If your search turns up
pk_live_, the Supabaseanonkey, or your FirebaseapiKey, that's expected. Don't rotate them; don't lose sleep over them. - Check for published source maps. In the Sources tab, if you can see your original, readable code (not minified) or files ending in
.map, your source maps are public — and any secret in them is exposed even if it's not visible in the bundle. - Search your repository, too. Look through your project for hardcoded keys and for any
.envvalue behind aVITE_,NEXT_PUBLIC_, orREACT_APP_prefix that holds a secret. Those prefixes ship to the browser.
This manual check catches the obvious cases. It won't catch everything — keys can hide in lazily-loaded chunks, third-party scripts, or API responses — which is the gap an automated scan closes.
Not sure if your app is leaking a secret key?
Run a free, read-only scan of your live app — no install, results in under a minute. Is My Site Hackable? tells you which keys are real leaks and which are public by design.
Scan my app free →How to find and rotate a leaked key
If you found a secret key in your frontend, here's the order of operations. The full playbook lives in a key just leaked — rotate, audit, monitor, but the short version:
- Rotate the key first. Generate a new secret key in the provider's dashboard and revoke the old one. This is the single most important step, because it makes the exposed key worthless. Do it before anything else.
- Move the new key server-side. Don't paste the new key back into frontend code — you'd recreate the same hole. Secret keys belong in a backend route, a serverless function, or a Supabase edge function, where the browser never sees them. Your frontend calls your server, and your server holds the key.
- Set hard usage limits. For metered services like OpenAI and Anthropic, set a spending cap so a future leak can't run up an unbounded bill. For Stripe, watch for unusual charges or refunds.
- Audit what the key could reach. Check your provider's logs for unfamiliar activity while the key was exposed. For a Supabase
service_roleleak, assume every table was readable and review accordingly. - Then watch for recurrence. Because your AI builder ships new code on every deploy, the same gap can come back. The point isn't to fix it once; it's to keep it fixed.
Per-provider quick guidance
A fast reference for the keys you're most likely to be worried about.
Stripe. pk_live (publishable) is public by design — meant for the browser, not an emergency. sk_live (secret) is full account access — if it's in your frontend, rotate it now and move it server-side. The prefix tells you which is which. Details: did I leak my Stripe key?.
Supabase. The anon key is public by design; your protection is RLS, not key secrecy. The service_role key bypasses RLS and must stay server-side. If you're unsure whether your Supabase project is exposed, start with is my Supabase exposed?.
Firebase. The web config and apiKey are public by design; your protection is Firebase Security Rules. Don't rotate the config — fix the rules. More: your Firebase web config is public by design.
OpenAI / Anthropic. Always secret, always metered. They must live server-side, behind hard usage limits. If one leaked, rotate it and check your usage. Full breakdown: an exposed OpenAI or Anthropic key is someone else's bill.
FAQ
Is it bad that my API key is visible in the browser?
It depends entirely on the key. Publishable keys — Stripe pk_live, the Supabase anon key, the Firebase web config — are meant to be visible in the browser; that's not a leak. Secret keys — Stripe sk_live, Supabase service_role, OpenAI and Anthropic keys — should never be visible there. Check the prefix and the key type before you panic.
How do I know if a key is public or secret?
Look at the prefix and the provider's docs. pk_ means publishable (public). sk_ means secret. The Supabase anon key and Firebase web config are documented as public by their own makers. Anything that can charge money, read all your data, or spend on a metered AI account is secret and must stay off the frontend.
Does minifying or obfuscating my code hide the key?
No. Minification shrinks code; it doesn't encrypt it. Anyone can read a minified bundle with browser dev tools, and a simple text search finds key patterns instantly. If you published source maps, your original, un-minified code is readable too. The only real protection for a secret is to keep it off the frontend entirely.
I rotated the key. Am I safe now?
Rotating revokes the exposed key, which is the critical step. After that, move the new key server-side so you don't recreate the leak, set usage limits, and check the provider's logs for any activity while the old key was exposed. Then keep scanning, because new deploys can reintroduce the same gap.
The bottom line
Not every exposed key is an emergency — and knowing the difference is the whole game. Stripe pk_live, the Supabase anon key, and the Firebase web config are public by design; finding them in your frontend is normal. The keys that matter are the secret ones — Stripe sk_live, Supabase service_role, and OpenAI or Anthropic keys — and if one of those is in your browser code, the fix is fast: rotate, move it server-side, set limits, audit. An attacker can scan your bundle for exposed secrets in minutes. The point is to find them first, and to keep checking, because your AI ships new code — and sometimes a new leaked key — on every deploy.
Find your gaps before an attacker does.
Is My Site Hackable? scans your deployed app for the exact issues in this article — exposed keys, missing RLS, open buckets — and tells you what's real and what's a false alarm.
Run a free scan →