Guide

Which Supabase keys are safe in the browser

Supabase gives you two keys that look almost identical. One is supposed to be in your app where anyone can read it. The other ends your security model the moment it ships. Telling them apart takes ten seconds.

The short version

KeyIn the browser?What it can do
anonYes — by designExactly what your Row Level Security policies allow, and nothing more.
service_roleNeverEverything. It bypasses RLS entirely — read, edit and delete any row in any table.

Why the anon key being public is fine

This trips people up, because it looks like a secret. It isn’t. The anon key only says which projectyou’re talking to. What you’re allowed to do once connected is decided entirely by Row Level Security, on the server, per table, per row.

Which is why the anon key being public and RLS being off is the dangerous combination — not either one alone. The key is the door handle. RLS is the lock.

If your RLS policies are right, someone copying your anon key out of your JavaScript gets nothing they couldn’t already see. If they’re missing, that key reads every row of every table.

How to tell which one you shipped

Both are JWTs — three chunks separated by dots, starting eyJ. You can’t tell by looking at the start. The role is in the middle chunk.

  1. Open your live app, press F12, go to the Network tab, reload.
  2. Find a request to *.supabase.co and copy the long key from the apikey header.
  3. Paste it into any JWT decoder, or just read the middle section. If it says "role": "anon", you’re fine. If it says "role": "service_role", stop and read the next section.

A faster tell, if you use Supabase’s newer publishable/secret key format: anything beginning sb_secret_ belongs on a server and nowhere else.

If the service_role key shipped

Treat it as compromised. It has been downloaded by every visitor and every crawler that has touched your site, and deleting the line from your code does not un-send it.

  1. Supabase dashboard → Settings → API → roll the service_role key. Do this first. Until you do, everything else is decoration.
  2. Remove it from your frontend code, and move any call that needed it into an edge function or server route that reads it from an environment variable.
  3. Turn RLS on for every table holding personal data, on the assumption that someone already looked.
  4. Check your Supabase logs for requests you don’t recognise. If personal data was reachable, you may have a disclosure obligation — that’s a question for a lawyer, not a scanner.

Why AI builders get this wrong

Not carelessness — ambiguity. Ask for “an admin page that lists all users” and there are two correct answers: write an RLS policy granting an admin role, or use the key that ignores RLS. The second works immediately and the code looks clean. Nothing in the preview tells you the difference, because in the preview both do exactly what you asked.