Supabase anon key vs service_role: which one is safe in the browser
One Supabase key is publishable; the other bypasses all your security rules. How the mix-up happens in AI-built apps and what to do if the wrong one leaks.
Supabase hands every project two API keys that look almost identical — two long eyJ... strings sitting next to each other in your dashboard. One of them is designed to be public. The other one is the master key to your entire database. Mixing them up is the single most damaging mistake we see in AI-built apps, and AI coding assistants make it disturbingly easy to do.
Short answer: the anon key is safe in the browser; the service_role key is never safe there. The anon key is filtered by Row Level Security on every request, so on its own it grants almost nothing. The service_role key bypasses all your security rules and can read, change or delete your entire database — it must only ever run in server-side code.
Here is the difference in full, how to check which one your app is exposing, and what to do if it's the wrong one.
Anon key vs service_role: the difference in one paragraph each
The anon (anonymous) key is publishable. It is meant to ship in your frontend — every Supabase tutorial puts it in the browser, and that is correct. On its own it grants almost nothing: every request made with it goes through Row Level Security (RLS), the per-row permission rules on your tables. The anon key says "I am an anonymous visitor"; RLS decides what such a visitor may see.
The service_role key bypasses RLS entirely. It exists for trusted server-side code — batch jobs, admin scripts, webhooks. Whoever holds it can read, modify and delete every row in every table, ignore all your security rules, and generally act as the database owner. It must never, under any circumstances, appear in code a browser can download.
The problem: both are JWTs from the same project, both start with eyJ, and both "work" if you paste them into a Supabase client. An app with the service_role key in the frontend functions perfectly — which is exactly why the mistake survives until someone hostile finds it.
How the mix-up actually happens
The most common path is an AI assistant fixing an error the fast way. Something server-side needs elevated access, the model grabs whichever key is in context, and it ends up in a file like:
NEXT_PUBLIC_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIs...
That NEXT_PUBLIC_ prefix (or VITE_ / REACT_APP_ in other frameworks) means "include this in the JavaScript bundle every visitor downloads." If the value is your service_role key, your database is now effectively public — RLS and all your careful rules included, because this key ignores them.
The second path is simpler: RLS was never enabled on a table. Then even the anon key can read and write everything in it. The anon key is only safe because of RLS; anon key + RLS off is the same catastrophe with fewer steps. (Supabase warns about this in the dashboard — the warning is worth taking seriously.)
How to tell which key you're looking at
A JWT is readable by anyone — that's the point of the format. Paste the key into a decoder (or just look at your Supabase dashboard: Settings → API labels them clearly) and check the role claim in the payload:
"role": "anon"— publishable, fine in the browser."role": "service_role"— server-only. If this string appears anywhere in your frontend bundle, your DevTools sources, or aNEXT_PUBLIC_-prefixed variable, you have a critical leak.
The 60-second manual check: open your deployed site, DevTools → Sources, search the JavaScript for service_role. Or run OpzyAI's free scan — it downloads your client bundle the way any visitor's browser does and specifically distinguishes the two keys: the anon key is on its allowlist and never flagged (flagging it would be a false positive — it's supposed to be there), while a service_role JWT in a public bundle is flagged as critical.
If your service_role key is exposed
Treat it as fully compromised — assume it has been copied:
- Rotate it now. Project Settings → API → regenerate the JWT secret. This invalidates both keys; grab the new ones and update your server config. Full order-of-operations in the leaked-key runbook.
- Move the code that needed it server-side. Whatever feature "needed" service_role in the browser belongs in an API route, edge function or server action, where the key stays in a private env var (no
NEXT_PUBLIC_prefix). - Check your data. Database → Logs, plus a look through your tables for rows you didn't create or changes you didn't make.
- Turn RLS on everywhere. Every table, even the ones that feel harmless. RLS is the seatbelt that makes the anon key safe.
The rule to remember
The anon key is public because RLS guards every request it makes. The service_role key bypasses those guards — so it can never live anywhere the public can look.
If you take one habit from this: whenever a Supabase key gets added or moved in your project — by you or your AI assistant — check the role claim before you deploy. It takes ten seconds and it's the difference between a public key and a public database.
Frequently asked
- Is the Supabase anon key safe to expose in the browser?
- Yes. The anon key is publishable and is meant to ship in your frontend. On its own it grants almost nothing, because every request it makes is filtered by Row Level Security (RLS). It is only safe, however, if RLS is actually enabled on your tables.
- What is the difference between the Supabase anon key and the service_role key?
- The anon key is public and every request it makes goes through Row Level Security. The service_role key bypasses RLS entirely and can read, change or delete every row in every table. The anon key belongs in the browser; the service_role key must only ever run in server-side code.
- What do I do if my Supabase service_role key is exposed?
- Treat it as fully compromised. Regenerate the JWT secret in Project Settings → API (this invalidates both keys), update your server config with the new keys, move the code that needed it server-side, check your database logs for misuse, and enable RLS on every table.
- How can I tell which Supabase key I have?
- A Supabase key is a JWT — decode it and read the "role" claim. "role": "anon" is the publishable key; "role": "service_role" is the server-only key. If a service_role key appears in your frontend bundle or a NEXT_PUBLIC_ variable, that is a critical leak.