Exposed Supabase keys: what anon vs service_role really means
You built an app with Lovable, Bolt or Replit, it uses Supabase, and now something — a tweet, a scanner, a nervous feeling — is telling you your keys are "exposed." Before you panic: not every exposed Supabase key is a problem. One of them is meant to be public. The other can hand your entire database to a stranger. Here's how to tell which one you're dealing with.
The two Supabase keys, in plain English
Every Supabase project ships with two API keys, and the whole story lives in the difference between them.
- The
anonkey (sometimes called the "publishable" key) is designed to live in your frontend. It is public by design — it's supposed to end up in the browser. It's safe, as long as Row Level Security (RLS) is switched on to control what it can actually see. - The
service_rolekey is the secret one. It bypasses RLS entirely. Any request made with it has full, unrestricted access to every table — read, edit, delete. It must only ever live on a server.
The anon key in your frontend is fine if RLS is on. The service_role key in your frontend is a five-alarm fire.
Why this hits AI-built apps hardest
AI builders scaffold a working app in minutes, but they optimize for "it works," not "it's locked down." Two things go wrong again and again:
- RLS never gets turned on. With RLS off, even the public
anonkey can read — and sometimes write — your entire database. The key was never the problem; the missing lock was. - The
service_rolekey ends up in the frontend. It gets pasted into client code to "make something work," and then it's baked into the JavaScript bundle your browser downloads — visible to anyone who opens their developer tools.
If you didn't know these two keys were different, that's not on you — the tools rarely stop to explain it.
What actually goes wrong
When the service_role key is sitting in your bundle, the attack takes no skill at all:
Anyone can open your site, find the key in the downloaded code, and query your database directly with full admin rights. No login, no exploit — just your own key used against you. They can dump every user's personal data, quietly edit records, or delete the whole thing.
How to tell if you're affected
A few quick things to look at:
- Open your live app, then your browser's DevTools → Network (or "view source"), and search the downloaded JavaScript for your Supabase URL. If a long
eyJ…key sits next to it, check which key it is. - In the Supabase dashboard under Authentication → Policies, if your tables say "RLS disabled," that's the other half of the problem — even the public key can then reach your data.
Doing this reliably by hand is fiddly. Keys are minified, split across bundles, and telling an anon key from a service_role key by eye isn't obvious — the difference is buried inside the token. It's exactly the kind of thing that's easy to get wrong when you're not sure what you're looking for.
Not sure which key your app is exposing?
Paste your app's URL and our free scan checks it in about 10 seconds — whether a dangerous key or an open database is visible to the public.
Scan your app free →How to fix it, if you found one
- Rotate the key immediately. If your
service_rolekey was exposed, roll it in the Supabase dashboard (Settings → API) and treat the old one as compromised — assume someone already has it. - Move it to the backend. The
service_rolekey belongs in server-side code only: an Edge Function, an API route, a server environment variable — never in anything the browser downloads. - Turn on RLS for every table, then write policies so the public
anonkey can only touch the rows it should. This is what makes the public key safe. - Check again. Once you've rotated and locked down, re-scan to confirm nothing is still exposed.
Supabase's own docs on securing your data go deeper on writing RLS policies if you want the full reference.