Security basics
An API key exposed in your frontend: what 30,998 apps shipped
An API key exposed in your frontend is usually a Google Maps key. We scanned 30,998 live vibe-coded apps and counted which secrets actually leak.

In short
- An API key exposed in your frontend is nearly always the harmless kind. We found a key worth naming in 1,332 of 30,998 apps, and 1,080 of those carried a Google API key and nothing else.
- The Supabase service_role key, the one every tutorial warns about, appeared in 3 apps out of 30,998. A Stripe secret key also appeared in 3.
- Keys that spend money or read data by their own shape appeared in 54 apps. An open Supabase table appeared in 2,096.
Someone opens your app, presses F12, and tells you there is an API key exposed in your frontend. The word they use is usually "leaked". They rarely say which key, and the advice you find afterwards treats every one of them as the same emergency.
They are not the same emergency, and we can now put numbers on the gap. Between 12 and 14 August 2026 we ran nine external checks over 30,998 live apps built with Lovable, Bolt, v0, Replit and Base44, and read the JavaScript each one hands to a browser. This is what was in it.
Is an API key exposed in your frontend actually a problem?
Usually not, and the shape of "usually" is more lopsided than we expected.
We found a key worth naming in 1,332 of the 30,998 apps. In 1,080 of those, the only thing we found was a Google API key, which is the credential that is supposed to be in your page. What protects that one is a setting held on Google's side, and hiding it was never part of the arrangement.
The other 29,666 apps shipped nothing our secrets check treats as a problem.
That figure needs one qualification to be honest. Publishable keys are excluded
from it: a Supabase anon key or a Stripe pk_ key belongs in the browser, so
we mark it as something you got right and it never enters these counts.
What we counted, and what we could not
We loaded each app the way a visitor does, in a real browser, and read the JavaScript it downloaded. Everything below is a shape found in that code.
We match key formats we know. Stripe, OpenAI, Anthropic, AWS, Google and Supabase have recognisable prefixes, and a Supabase JWT states its own role in readable text in the middle section. A credential in a format we do not recognise is not in these numbers, so read them as a floor.
We never used a key we found. Not once, on any app. We recorded the type
and a masked hint of the form sk_live_…a1b2, and the real value was never
written down anywhere.
No app is named. Not here, not in the dataset, not anywhere we publish.
Nearly all of these are apps published on a builder's own domain. The full method, the sample and every figure behind this article are in our scan report, including the parts we could not measure.
What the 30,998 apps were actually shipping
One table, ordered by how often we saw each thing. The secrets check completed on every one of the 30,998 apps, so every count below is out of all of them.
| What we found in the browser code | Apps |
|---|---|
| Google API key | 1,142 |
| A high-entropy value beside a "secret" or "password" name | 204 |
| OpenAI key | 33 |
| AWS access key | 9 |
| Anthropic key | 5 |
Supabase service_role key | 3 |
| Stripe secret key | 3 |
| Stripe restricted key | 2 |
The rows add up to more than 1,332 because one app can carry two of these. Sort the same 1,332 apps into buckets that do not overlap and the picture gets starker: 1,080 had a Google key and nothing else, 198 had a high-entropy value that may or may not be a real credential, and 54 had a key that is a genuine secret by its own shape.
That second bucket is worth naming carefully. A high-entropy value sitting next
to a word like secret or password might be a live credential, or it might be
a session identifier, a build hash or a public token with an unlucky name. We
flag it as worth a look, and from outside nobody can tell you which it is.
The 54 in the third bucket are the real thing. All but two of them came out graded D or F, because a single critical finding caps the grade at D no matter what else the app got right. The largest single group inside it is the OpenAI key, at 33, and that one has no setting that would make it safe in a browser.
The key everyone warns about was the rarest thing we found
Three apps. That is how many shipped a Supabase service_role key, the one that
walks past every table rule you have written. A Stripe secret key also appeared
three times.
Set that against the other half of the same sweep. Of the apps we scanned, 8,429
named a Supabase project, and both findings sit inside that one group. Three of
them shipped the service_role key. In 2,096 of them, at least one table
answered a request carrying no login at all, and in 394 that table was named
after people: users, profiles, customers, orders.
The 2,096 is a floor. 4,749 of the 8,429 never answered our database check, for
reasons we cannot see from outside, and those are recorded as unknown rather
than as clean. The three service_role keys are an exact count, because a key
is read out of code that every app hands over.
Why the warnings point at the wrong key
We only see the outside of these apps, so we cannot tell you why. What we can show is which mistake survives.
Copying the wrong Supabase key is genuinely easy to do. In an older project
anon and service_role sit next to each other in the same dashboard panel,
they are the same length and the same shape, and
nothing breaks if you take the wrong one.
It still only happened three times in 30,998 apps, because nothing pushes you
towards it. Pasting either key makes the app work.
The open table has a force behind it. Row Level Security gets switched on, the app stops showing data, a policy that allows everybody gets written to make it work again, and from that moment on the dashboard reports the table as protected. Switching it on is not the same as being protected. Our scan grades that table a critical finding on a day your dashboard is showing you the switch turned on and a policy in place.
How to find an exposed API key in your app, for free
Start by hand, because it costs five minutes and needs nothing installed. Open
your live site, view the page source, and search it for four strings: AIza for
a Google key, sk_ for a Stripe or model-provider secret, service_role for
the Supabase key that ignores your rules, and eyJ for any Supabase token.
Anything that comes back is already in the hands of every visitor you have.
That gets you the page itself. What it misses is the JavaScript the page loads
afterwards, which is why our own scanner opens an app in a real browser and
reads the bundles instead of the HTML. Searching by hand also cannot tell you
whether the eyJ token you found is the anon key or the secret one, because
the two are the same length and the same shape.
Our free scan covers both. It loads your app in a real browser, reads the code that actually arrives, decodes each Supabase token and reports the role written inside it, so a publishable key comes back marked correct instead of buried in a wall of red. You get a grade, a score and the counts on screen in about 20 seconds with no account. Give an email address and you get the detailed list too, with a fix written for your builder that you can paste straight in.
Three things it will not do, which are the reasons it is safe to run on a live
app: it never logs in, it never writes anything, and it never keeps a key it
finds. An exposed secret is stored as a masked hint like sk_live_…a1b2, and
the real value is discarded. Scan your app, or read
what each of the nine checks looks at first.
What to do, in the order the numbers suggest
What to do
- Identify the key before you react to it. The prefix answers it for Stripe and for newer Supabase keys; for an older Supabase key, the
rolefield inside the token decides. - If it is a Google key, restrict it rather than hiding it. Websites restriction, your domain, only the APIs you use, plus a daily quota cap. Free, about five minutes, no code change.
- If it is a genuine secret key, rotate it first. Deleting it from your code closes nothing, because the old value is still in your version history and in cached copies of your site.
- Then go and read your table rules, which is where the numbers say the real exposure is. Start with the tables holding people.
- Check billing and logs after any secret-key exposure. Rotation stops what happens next, and says nothing about what already happened.
Work through the whole list in one sitting with the 10-minute security checklist, which covers the key and the table rules alongside the other things worth closing in a newly launched app. And if the database half of this article is the part that worries you, it has a census of its own: can anyone read your Supabase database.
FAQ
How do I know if my API key is leaked?
Open your live site, view the page source, and search it for the shapes: AIza for a Google key, sk_ for a Stripe or model-provider secret, and eyJ for a Supabase JWT. Anything that turns up is in the hands of every visitor already. Our free scan does the same read from outside and reports what it can see in about 20 seconds, with no account needed for the grade.
Is a hardcoded API key always a security problem?
No, and treating it that way is how people learn to ignore the warning. Some keys are published on purpose: a Supabase anon key, a Stripe pk_ key and a Firebase web config key are all designed to sit in the browser, and what protects your data is the rules behind them. A secret key is the opposite case and belongs on a server. The prefix tells you which one you are looking at.
Someone said my Supabase key is exposed. Is that the bad one?
Almost certainly not. Both Supabase keys look alike, so read the role inside the token or check the prefix: anon and sb_publishable_ are meant to be public, service_role and sb_secret_ are not. We found a service_role key in 3 apps out of 30,998, so the odds are heavily on the harmless one. If it is the secret one, rotate it in the Supabase dashboard today.
What should I actually worry about instead?
The table rules on your database. Of the apps where we could complete that check, more than half had at least one table that answered a request carrying no login at all, and in 394 of them the open table was named after people: users, profiles, customers, orders. That is far commoner than any leaked key and it is much quieter, because the app works exactly the same either way.
I removed the key from my code. Is it closed now?
Not on its own. The old value still exists in your version history and in any cached copy of the page, so anyone who already collected it can keep using it. Rotating the key in the provider dashboard is what actually closes the door, and it is the first step rather than the last. After that, check billing and logs for usage you cannot account for.