Two Supabase keys, the same shape, opposite consequences: how BreachProbe tells them apart
A stranger can open your app, view the source, and find a key to your database sitting there in plain text. That is fine. That is how the app is supposed to work. It stops being fine when the key sitting there is the other one, and the two are close to indistinguishable by eye.
BreachProbe is a security scanner for apps built fast with AI coding tools. You paste a URL, it reads the JavaScript you already serve to the public, and it tells you what a stranger can reach. The first thing it has to do is decide which of the credentials in that bundle are supposed to be there.
Watch the BreachProbe, Find out if your app leaks its database demo
The difference is one word, and it is encoded
Supabase's legacy keys are both JWTs. Both begin eyJ. Both are roughly the same length, both are the same character class, both match any regular expression you would write to catch "a credential". One of them is the browser key, published on purpose, and shipping it is correct. The other carries the role service_role, and it walks past every row-level security policy you wrote, which is the database rule deciding who may read which rows. Anyone who copies it can read, edit and delete everything.
So the scanner does not pattern-match. It splits the key at its dots, base64-decodes the middle segment, and reads the role field out of it. Role anon produces no finding at all. Role service_role produces a critical, and any critical caps the letter grade at F no matter how clean the rest of the app is.
A scanner that reported every JWT in the bundle would fire on every correctly built Supabase app in existence. That is the check refusing to be noise.

Then the ecosystem changed the shape
Supabase now issues a different key format: sb_publishable_ for the browser, sb_secret_ for the server. Its own documentation, retrieved 12 August 2026, says the legacy pair "will be deprecated by the end of 2026, and you should now use the publishable and secret keys instead."
The end of 2026 has not happened, so both formats are live in the wild at once. And the decoding trick above does not transfer: sb_secret_ has no middle segment, nothing to decode, nothing to read a role out of. It is caught on the prefix alone. Any checklist or scanner that learned only the old shape reads straight past one.
| Found in the shipped JavaScript | What it is | What BreachProbe does |
|---|---|---|
Legacy JWT, role anon | the browser key, meant to ship | nothing, this is correct |
Legacy JWT, role service_role | bypasses every row-level security policy | critical, grade capped at F |
sb_publishable_… | the new-format browser key | nothing, this is correct |
sb_secret_… | the new-format server key, nothing inside to decode | critical, caught on the prefix |
A Stripe key beginning sk_live_ | can create charges and read every customer | critical |
What you get, and what it costs
The scan and the score are free. The full report is $19, one payment, and it locates every finding and hands you a written fix per issue, phrased as an instruction you paste back into Lovable, Cursor, Bolt or Claude Code. $39 adds thirty nights of re-scans and an email the moment something new appears.
One more refusal worth naming: a host BreachProbe could not reach gets no number. Not a zero, not an F. The report prints a dash, because nothing was measured.
Scan your own app: https://breachprobe.thecompound.tech/hn?utm_campaign=compound
---
One shipped product, taken apart, once a month. What it does, what it cost to build, what the pipeline behind it looks like, and what the numbers did, read off the repository and the live site, not written from memory. Join the list.