Home / RLS / Is the anon key safe to expose?
Supabase RLS - Fundamentals
Is the Supabase anon key safe to expose?
Yes. The anon key is designed to live in the browser, and seeing it in your JS is not a leak. It's safe only while RLS is on, with a policy, on every table it can reach. The key never grants access on its own; a table with RLS off does. The key you must never ship is service_role.
01 Short answer
Yes. The anon key (and the newer sb_publishable_... key) is public by design - Supabase expects it in your frontend bundle, and anyone can read it from your site's JavaScript. That by itself is not a vulnerability.
The catch: the anon key is only safe because Row Level Security is supposed to stand between it and your data. If RLS is off on a table the key can reach, that public key reads every row in it (and writes too, if the table also grants insert/update/delete to anon). So the honest answer is "yes, if RLS is on everywhere it can reach" - not "yes, always."
02 Why it's safe when RLS is on
The anon key only tells Supabase which role the request runs as (anon, the signed-out visitor). What that role can actually see is decided by RLS. Proven on Postgres:
what anon sees, by configuration
-- RLS OFF, anon has a grant: anon reads EVERY row (leak) -- RLS ON, no policy: anon reads 0 rows (deny by default) -- RLS ON, policy for authenticated: anon reads 0 rows (policy doesn't match anon) -- RLS ON, policy scoped to the user: each user reads only their own rows
So with RLS on and a correct policy, the public key is exactly as powerful as you decide - usually "read the public catalog, nothing private." That's why Supabase ships it to the browser without worry.
03 Where the real danger is
A table with RLS off
This is the actual leak behind almost every "Supabase app exposed N users" story. RLS off + a grant to anon = the whole table is public to anyone with your key, which is everyone. Why a table is public.
The service_role key in the browser
Different key, different story. service_role bypasses RLS entirely - it reads and writes every table no matter what. It must live only on a server, never in frontend code or a NEXT_PUBLIC_ variable. If it's in your bundle, rotate it now. Scan your site for it.
RLS on, but a permissive policy
A policy of USING (true) has RLS "on" and still lets everyone read every row. Fix a permissive policy.
Note the Oct 30, 2026 platform change doesn't fix this: new tables stop being auto-exposed, but a table that already has RLS off stays open. What changes on Oct 30.
04 Check your own app in two ways
In the SQL editor, list every table and view and whether anon can read it with RLS off:
check.sql
select c.relname, has_any_column_privilege('anon', c.oid, 'select') as anon_can_read, c.relrowsecurity as rls_on from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname = 'public' and c.relkind in ('r', 'p', 'v', 'm') order by 1;
Any row with anon_can_read = true and rls_on = false is open to the public key. From outside, the free scanner checks this from outside against your live app - a leaked service_role key, and tables anyone can read without logging in - using only the public key your app already ships, and reading only row counts, never your data.
05 Turn an open table safe
Enable RLS and add a policy that scopes rows to the signed-in user, in the same migration:
migration.sql
alter table public.notes enable row level security; create policy "owner reads own" on public.notes for select to authenticated using (user_id = (select auth.uid()));
Signed-out visitors (anon) now read nothing from notes; signed-in users read only their own rows. The anon key is still in your bundle - and now it's genuinely safe. Full set: owner-scoped policy for all four operations.
Is your app leaking right now?
Paste your live URL. The scanner reads your page and its JavaScript for a Supabase service_role key, the one leak that skips every policy in this guide.
Catch this before it ships
airlock-rls is a CI gate that fails your build when a table ships exposed or a policy is permissive: the same class of bug, caught in CI against a database with your migrations applied, before it reaches prod.
Or start from nextjs-supabase-starter : auth + a table with RLS + an isolation test, so you start from a pattern that's already sealed.
Grab the Supabase RLS cheat sheet
The golden rules, the footguns that leak in prod, correct policy snippets, and the isolation test — on one page.
FAQ
Someone found my anon key in the browser. Is that a hack?
No. The anon key is meant to be in your frontend; every visitor's browser downloads it. Finding it is expected. What matters is whether RLS is on for every table it can reach. Run the check above. If no table shows anon_can_read true with rls_on false, nothing is exposed by RLS being off - then also confirm you have no permissive USING (true) policies (section 03), which stay readable even with RLS on.
What's the difference between the anon key and the service_role key?
The anon key runs requests as the signed-out 'anon' role, fully subject to RLS - so it's safe in the browser when RLS is correct. The service_role key bypasses RLS entirely and can read and write everything; it must stay server-side and never ship to the browser.
Do I still need RLS if I only use the anon key?
Yes - RLS is exactly what makes the anon key safe. Without RLS (or with a permissive policy), the public key reads every row. The key and RLS aren't alternatives; the key is safe because RLS limits it.
Is the new sb_publishable_ key any different?
No, for this question. It's the modern name for the public client key and is also safe to ship to the browser under the same condition: RLS on, with a policy, on every table it can reach. The secret counterpart, sb_secret_, is the one to keep server-side.