Updated October 20, 2026 · ClearanceHQ
sb_publishable_ key) is meant to be public. It is safe only if Row Level Security (RLS) is enabled, with correct policies, on every table your app exposes. The key that must never be public is the service_role / sb_secret_ key.Why the anon key is public by design
Supabase apps talk to the database straight from the browser, so the browser needs a key. The anon (publishable) key only identifies your project and lets requests run as the low-privilege anon role (or authenticated, once a user signs in). What that role can actually read or write is decided by your RLS policies and table grants, not by keeping the key secret. Anyone can copy it from your site's JavaScript in seconds.
That's why ClearanceHQ never reports a Supabase anon key as a leak. We report what the key can reach.
Which key do you have?
sb_publishable_…or a legacy JWT whose payload says"role":"anon": public, fine in the browser.sb_secret_…or a legacy JWT with"role":"service_role": bypasses RLS. Server only.
Legacy keys are JWTs (they start with eyJ); the middle part between the two dots is base64 and contains the role. Decode it locally rather than pasting a key into a website. Supabase says it is deprecating the legacy anon and service_role keys by the end of 2026 in favour of publishable and secret keys.
Check 1: is RLS enabled on every table?
Run this in the Supabase SQL editor:
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;
Every table should show rowsecurity = true. Then list the policies:
select tablename, policyname, cmd, roles, qual
from pg_policies
where schemaname = 'public'
order by tablename, policyname;
Watch for qual = true (that is, using (true)) on tables that hold private data. Tables you created in the Table Editor have RLS on by default, but tables created with SQL, migrations or an AI agent's script often don't.
Check 2: test it as a stranger
The real test is what an anonymous visitor gets. Use only the public key, exactly as your site does:
curl -s "https://YOUR_PROJECT.supabase.co/rest/v1/YOUR_TABLE?select=*&limit=1" \
-H "apikey: YOUR_ANON_OR_PUBLISHABLE_KEY"
If a private table returns a row, anyone on the internet can read it. An empty [] means no rows are visible to anonymous users (or the table is empty). To count visible rows without downloading any data, ask for a count instead:
curl -sI "https://YOUR_PROJECT.supabase.co/rest/v1/YOUR_TABLE?select=*" \
-H "apikey: YOUR_ANON_OR_PUBLISHABLE_KEY" \
-H "Prefer: count=exact" -H "Range: 0-0"
The content-range header ends with the number of rows the public key can see, for example */0 (none) or 0-0/1532 (all 1,532 rows are readable). Only test projects you own.
Check 3: the Security Advisor
In the Supabase dashboard, open Advisors → Security Advisor. It flags tables in exposed schemas with RLS disabled, and other risky settings. Fix everything marked as an error before launch.
How to fix an open table
alter table public.notes enable row level security;
create policy "Users read their own notes"
on public.notes for select to authenticated
using ( (select auth.uid()) = user_id );
create policy "Users add their own notes"
on public.notes for insert to authenticated
with check ( (select auth.uid()) = user_id );
With RLS enabled and no policy for an action, that action is denied, so add a policy for each thing users should be able to do (select, insert, update, delete) and nothing more. Re-run Check 2 afterwards.
Mistakes we see most
- Tables created with SQL never got RLS. Enable it explicitly in every migration.
- Policies set to
using (true)onprofiles,ordersormessages: that is "allow everyone". - Views that skip RLS. Views run with their owner's privileges by default. On Postgres 15+ create them
with (security_invoker = true), or keep them out of exposed schemas. security definerfunctions callable through/rest/v1/rpc/: RLS doesn't apply inside them. Grantexecuteonly to the roles that need it.- Policies that trust
raw_user_meta_data. Users can edit their own user metadata, so don't base access rules on it. - The service_role key in frontend code, often added by an AI agent to make a "permission denied" error go away. Remove it, rotate it and fix the policy instead.
- Public storage buckets holding private uploads.
What ClearanceHQ checks
The full scan finds the Supabase URL and anon or publishable key in your live JavaScript, lists the tables that key can see, and counts the rows it can read using a count-only HEAD request. We never download row contents. In your repo it looks for RLS-disabled tables, permissive policies and service_role or secret keys. The free check is URL-only and doesn't test Supabase.
Quick answers
Is the Supabase anon key safe to put in my frontend?
Yes. The anon key (and the newer sb_publishable_ key) is designed to be public and is visible to every visitor. It is only safe if Row Level Security is enabled, with correct policies, on every table in a schema exposed through the Data API (public by default).
Which Supabase key must never be public?
The service_role key or any sb_secret_ key. It bypasses Row Level Security completely, so it belongs only in server-side code and environment variables. If it was ever shipped to the browser or committed to git, rotate or revoke it.
How do I check if Supabase RLS is enabled?
In the SQL editor run: select tablename, rowsecurity from pg_tables where schemaname = 'public'; every row should show rowsecurity = true. Then test as a stranger: request a private table through the REST API with only the anon or publishable key and confirm no rows come back.
Does enabling RLS without any policies block access?
Yes. With RLS enabled and no policies, requests made with the anon or publishable key get no rows. You then add policies for exactly what each role should see or change.
Want this checked for you?
The free check looks at your live URL: headers, cookies and exposed .env or .git files. The full scan covers your repo, git history and live site for $1 $5, with an Agent fix prompt and a free re-scan.
Get the full scan Run the free check
Automated, read-only checks. A clean result does not guarantee your app is secure.