Is the Supabase anon key safe to expose? Yes, if RLS and grants are right. Here's how to check
Last updated · By Ember, SniffPup
Short answer: yes. The Supabase anon key, and the newer sb_publishable_ key that replaces it, is built to ship in your frontend, and Supabase's API keys docs call the publishable key safe to expose. It only reaches what Row Level Security and your table grants allow. So the real question isn't "is the key public?" (it is) but "what can someone do with it?" The keys that must never reach the browser are sb_secret_ and the legacy service_role key, because they bypass RLS.
Check it in 3 steps
- Make sure only the public key is in your frontend. No
sb_secret_orservice_rolekey in your JavaScript, a public.envfile or aNEXT_PUBLIC_/VITE_variable (which key is which). - Turn RLS on for every exposed table, with real policies. Then list any table with RLS off (SQL).
- Test it like a stranger. Use the anon key signed out, then as a second user, and run a free SniffPup scan of your live URL to see what an anonymous visitor can read (how).
SniffPup looks at your published site the way a stranger would: secret keys in your HTML and same-origin JavaScript (masked in the report), public /.env and /.git files, a downloadable source map and missing security headers. When your site ships its Supabase URL and public anon key, the free scan also sends one read-only HEAD request per table, up to 8, to see whether an anonymous visitor can read at least one row, without downloading rows or writing anything. It doesn't read your policies or grants. A score of 100 means no issues were found in our checks; it is never proof of security. Every critical issue comes with its full fix, free (with no critical, one other fix is free).
Which Supabase key is which
Per Supabase's API keys docs, every key maps to a Postgres role, and that role is what your policies match on:
- Publishable key (
sb_publishable_...) and the legacy anon key (a long JWT starting witheyJ): low privilege. Requests run asanon, or asauthenticatedonce a user signs in with Supabase Auth. Safe in a web page, mobile app or source code. - Secret key (
sb_secret_...) and the legacy service_role key: elevated. Requests run asservice_role, which has theBYPASSRLSattribute and skips every policy. Server, Edge Function or cron job only.
Supabase is deprecating the legacy anon and service_role keys by the end of 2026. If one of the elevated keys ever shipped to the browser, treat it as leaked: our leaked service_role key guide has Supabase's own rotation steps.
Why the anon key is only as safe as your RLS and grants
Anyone can copy your anon key out of your JavaScript and call your project's Data API directly, skipping your app's screens and any checks that only live in the browser. Postgres checks two things, in order (Securing your API):
- Grants decide whether the
anonorauthenticatedrole can reach a table at all. A missing grant returns a permission error. From October 30, 2026, Supabase stops granting newpublictables automatically on existing projects (changelog), so new tables need explicit grants. See our 42501 grants guide. - Row Level Security decides which rows that role gets. With RLS off, a granted role gets every row. With RLS on and no policy, it gets none.
Find tables the anon key could read in full, the first thing to fix:
select tablename from pg_tables where schemaname = 'public' and rowsecurity = false;
Turn RLS on and add a policy per operation with a real owner check, not using (true):
alter table public.your_table enable row level security; create policy "users read their own rows" on public.your_table for select to authenticated using ((select auth.uid()) = user_id);
Our Supabase RLS policy examples cover public read with owner-only writes, teams, admin roles and insert-only forms. Supabase's Security Advisor also flags tables with RLS off; review each finding before you dismiss it.
Test what a stranger can do with your anon key
Signed out, with your project URL and public key (only test projects you own):
curl "https://YOUR_REF.supabase.co/rest/v1/your_table?select=*&limit=1" \ -H "apikey: YOUR_PUBLISHABLE_OR_ANON_KEY"
- A permission error:
anonhas no grant on this table. Right for private data. []: the grant exists and RLS returned no rows (or the table is empty).- A row: anonymous visitors can read this table. Fine for a public catalog, a leak for profiles, orders or messages.
With a legacy anon key, also send it as Authorization: Bearer. Then sign in as two test users and make sure user B can't read or change user A's rows. Reads aren't the whole story: an update policy whose using clause is too loose lets a user change rows they don't own, and a with check looser than using (like with check (true)) lets them hand a row to someone else. Our RLS checker guide has the full set of SQL checks.
Is RLS enough, or should I go server-only?
This comes up a lot, and both sides have a point. Some developers would rather never expose the database to the browser at all: put every read and write behind their own API, use the secret key only on the server, and give anon no access. Others use the public key with RLS, which is the model Supabase designed the publishable key for. Here's a practical way to choose, table by table:
- RLS is usually enough when the rule fits in one sentence per row ("users see their own notes", "members see their team's projects", "anyone can read published posts") and you've tested it signed out and with two users.
- Move it server-side when the rule depends on things a policy handles badly: payments and credits, admin actions, rate limits, multi-step writes that must all succeed together, or calls to paid AI and email APIs. Put that action in a server route or Edge Function that checks the user, then revoke the
anonandauthenticatedgrants on tables only the server should touch. - Keep RLS on either way. Server-only access moves the risk into your own routes, which each need an auth check, and the secret key must never leak. With RLS on and no policies, a table your frontend shouldn't use fails closed instead of open.
If you can't move to a backend yet, the fast wins are the same: RLS on every table, the smallest grants, owner checks in policies, no secret key in the frontend, and a signed-out test after every schema change.
FAQ
Is the Supabase anon key safe to expose?
Yes, it's designed to be public. Supabase's API keys docs list the publishable key (sb_publishable_, the newer version of the legacy anon key) as safe to expose in a web page, a mobile or desktop app or source code. Anyone who opens your site can read it. What protects your data is Row Level Security and table grants on the anon and authenticated roles, so the key is only as safe as those are.
What's the difference between the anon key, the publishable key, service_role and sb_secret?
Per Supabase's docs there are four kinds. The publishable key (sb_publishable_) and the legacy anon key are low-privilege and map to the anon role, or to authenticated once a user signs in. The secret key (sb_secret_) and the legacy service_role key are elevated: they map to service_role, which bypasses every Row Level Security policy. Supabase is deprecating the legacy anon and service_role keys by the end of 2026.
Which Supabase key must never be in my frontend?
The secret key (sb_secret_...) and the legacy service_role key (a long JWT starting with eyJ). Both bypass Row Level Security, so anyone who finds one in your JavaScript can read and change every row. Supabase says a new secret key is rejected with HTTP 401 when sent from a browser, but someone can still use a leaked one from other tools. If one leaked, rotate it now.
Is RLS enough to protect my Supabase data?
For most app data, RLS with owner-scoped policies on every table, plus only the grants each role needs, is the protection Supabase designed for the public key. It's enough when your rules are simple to state per row (a user sees their own rows, a team sees its rows) and you test them. When the rule depends on payments, admin actions, multi-step workflows, rate limits or calls to paid outside APIs, move that table or action behind a server route or Edge Function that checks the user and uses the secret key, and give anon and authenticated no access to it.
Should I stop using the anon key and do everything server-side?
You can, and it's a reasonable choice for sensitive data, but it moves the risk rather than removing it. With server-only access every route you write has to check who's calling, and the secret key must stay on the server. Keep RLS on either way: with RLS on and no policies, the anon and authenticated roles get no rows, so a forgotten table fails closed.
Can SniffPup tell me whether my anon key exposes data?
It tests one part from the outside. When your live site ships its Supabase project URL and public anon or publishable key, the free scan reads the table list that key can see and sends one read-only HEAD request per table, up to 8, to learn whether an anonymous visitor can read at least one row. It never downloads rows and never writes. It also flags a service_role or sb_secret key in your page or JavaScript. It doesn't read your policies, test writes, or test signed-in users reading each other's rows.
Does a clean SniffPup report mean my Supabase project is safe?
No. A score of 100 means no issues were found in our checks. It is never proof of security, and it says nothing about tables the scan didn't reach, write access or what signed-in users can see.
Sources: Supabase API keys, Securing your API, Row Level Security, Supabase changelog: tables not exposed automatically, checked October 27, 2026. SniffPup checks what is publicly visible on your site. A clean report is not a guarantee of security.