anon key vs service_role key, explained for founders
Which Supabase key is safe in the browser, which one bypasses Row Level Security, and how to check your app in two minutes.
Every Supabase project comes with two kinds of API key. Mixing them up is the most common serious mistake I see in AI-built apps, and it's easy to make because both look like long random strings.
The short version
| Public key | Secret key | |
|---|---|---|
| Names | anon (legacy) or sb_publishable_… | service_role (legacy) or sb_secret_… |
| Safe in the browser? | Yes, by design | Never |
| Respects Row Level Security? | Yes | No, it bypasses RLS |
| Where it belongs | Your frontend | Servers, Edge Functions, CI |
Why the public key is fine to ship
The public key only identifies your project. What a request can actually do is decided by Row Level Security policies in your database. With RLS on and good policies, someone holding your public key can only do what a visitor or signed-in user is allowed to do.
That's also why RLS matters so much. With RLS off, the public key can read and write the whole table. Nothing in between is checking.
Why the secret key is an emergency
The secret key skips RLS entirely. It's meant for trusted server code that needs to do admin work, like a webhook that updates any user's subscription. If it ends up in your frontend bundle, anyone who opens dev tools has full read and write access to your database.
How it usually gets there in AI-built apps:
- The builder hit an RLS error, and “fixed” it by switching to the key that ignores RLS.
- An env var was named with a public prefix (
VITE_,NEXT_PUBLIC_), which bundles it into client code. - Someone pasted it into a config file that the frontend imports.
How to check in two minutes
- Open your live app, then your browser's dev tools, then the Sources or Network tab.
- Search all loaded JavaScript for
service_roleandsb_secret_. - For legacy JWT keys, paste any long
eyJ…string into a JWT decoder and look at theroleclaim.anonis fine.service_roleis not.
If you find it
- Rotate it in the Supabase dashboard right away. Removing it from the code isn't enough: it's already in old deploys and caches.
- Move whatever needed it into an Edge Function or server route that holds the new key as a secret.
- Fix the RLS policy that made the builder reach for it in the first place.
- Check your logs for unusual reads or writes while it was exposed.
This check is part of every Launch Risk Diagnostic. If you'd rather not do it alone, that's what it's for.