Is the Supabase anon key safe in the frontend? Yes, if RLS is on
Builders find the Supabase anon key in their JavaScript and panic, or see a scanner flag it and assume they've been breached. The key itself is meant to be public. What it can reach depends entirely on your Row Level Security, and that is what you need to test.
Short answer: Yes. The anon key (and its replacement, the sb_publishable_ key) is designed to ship in browser code. It only gets what your Row Level Security policies and table grants allow the anon role. The service_role JWT and sb_secret_ keys are the opposite: they bypass RLS and must never reach the browser.
This is the short answer page. For the full policy-by-policy walkthrough, use the Supabase RLS checklist.
Why the key is public by design
A Supabase app built with Lovable, Bolt or by hand talks to the database straight from the browser. For that to work, the browser needs your project URL and a key, so both end up in your JavaScript bundle where anyone can read them. Supabase's API key docs put it plainly: the publishable key is safe to expose, and anyone can read it, so it only reaches what Row Level Security allows.
Two key systems exist right now:
- Publishable (
sb_publishable_...) and its legacy form, the anon JWT (eyJ...): low privilege, for the browser. Requests use theanonPostgres role, orauthenticatedwhen a user is signed in. - Secret (
sb_secret_...) and its legacy form, the service_role JWT: full access, with theBYPASSRLSattribute. Server only. Supabase rejects new secret keys sent from a browser (it returns 401 based on the User-Agent); Supabase lists that check as one of the improvements over the legacy service_role JWT.
Supabase says it is deprecating the legacy anon and service_role keys by the end of 2026, so new projects should use publishable and secret keys. Creating new keys doesn't disable the old ones; that's a separate step in Settings > API Keys.
So what actually makes it unsafe?
The key is safe; a table without RLS is not. Supabase's RLS docs warn that a table in an exposed schema without RLS is readable and writable by any role with a grant on it. On projects that still grant anon broad privileges by default, that means anyone holding your public key. Adding policies doesn't remove grants, and a policy like to anon using (true) hands every row to every visitor.
Check it yourself: one query as anon [manual]
Run this against your own project, with the same URL and public key your frontend uses. It asks for rows exactly as a logged-out stranger would.
curl "https://YOUR-REF.supabase.co/rest/v1/profiles?select=*&limit=5" \
-H "apikey: YOUR_PUBLISHABLE_OR_ANON_KEY"
[]: RLS is on and no policy lets anon read. Usually what you want for private data.- Rows come back: anon can read this table. Fine for a public blog table; a leak for profiles, orders or messages.
- A permission error: the table has no grant for anon, which also blocks access.
Then list every table that has RLS switched off. Run this in the SQL Editor:
select schemaname, tablename
from pg_tables
where schemaname = 'public' and rowsecurity = false;
Any table in that list is open to whatever grants anon holds.
Fix it
- Enable RLS on every public table.
alter table public.profiles enable row level security; - Write policies per role and operation. Owner-only reads are the common case:
create policy "own profile" on public.profiles for select to authenticated using ((select auth.uid()) = id); - Re-run the anon curl. Private tables should now return
[]or an error. - If a service_role or sb_secret_ key was in the bundle, that is the real emergency: rotate it now. See Supabase service_role key exposed.
List every table in the public schema. For each one, enable Row Level Security and write explicit policies for select, insert, update and delete, scoped to auth.uid() for user-owned rows. Never use "using (true)" for private data. Make sure the frontend only uses the publishable or anon key and that no service_role or sb_secret_ key appears in any client file or VITE_ variable.
Make it stick
- Enable RLS in the same migration that creates a table, so a new table is never briefly open.
- Keep the anon curl as a quick test after any schema change.
- Files have their own policies: see Supabase Storage buckets.
What the free scan checks here (and what it doesn't)
The scan doesn't test tables. It can't tell whether RLS is on or what anon can read. It reports your Supabase URL and anon or publishable key as info only, with a reminder to check RLS. It flags a service_role JWT or sb_secret_ key in your HTML or up to 25 linked scripts as critical, and also checks other leaked secret keys, /.env, /.git/HEAD, HTTPS and four security headers. Testing table access is [manual].
FAQ
- Should I rotate my Supabase anon key if it's visible in my site?
- No, that's expected; every visitor's browser needs it. Rotating it doesn't protect data, because the new key is just as public. Fix your RLS policies instead. Rotate immediately if a service_role or sb_secret_ key was exposed.
- How do I tell an anon key from a service_role key?
- New keys say it in the prefix: sb_publishable_ is public, sb_secret_ is secret. Legacy keys are both long JWTs starting with eyJ; decode the middle part at a JWT decoder and read the role claim, which is anon or service_role.
- Does the free scan test my Supabase tables?
- No. The scan doesn't test tables or RLS. It reports your Supabase URL and anon key as info only, flags any service_role JWT or sb_secret_ key in your page or up to 25 scripts as critical, and checks /.env, /.git/HEAD, HTTPS and headers.