Is the Supabase anon (publishable) key safe to expose?
By StackSiren · Published
Yes. It's designed to be public. Supabase's docs describe the publishable key as "safe to expose online", and the legacy anon key as its older equivalent. But "safe to expose" has a condition attached. The key lets anyone talk to your database as the anon role. What they can actually read or change depends entirely on your grants, Row Level Security (RLS) and policies.
So if a scanner, an AI tool or a Reddit commenter has flagged your anon key, the key itself isn't the problem. The question is what your database allows that key to do.
Which key do you actually have?
Supabase has four kinds of API key. Two are meant to be public and two must never leave your server:
| Key | Looks like | Public? | Postgres role | RLS applies? |
|---|---|---|---|---|
| Publishable | sb_publishable_... (short string) |
Yes | anon, or authenticated with a user's session |
Yes |
Legacy anon |
Long JWT starting eyJ... |
Yes | same as above | Yes |
| Secret | sb_secret_... |
No | service_role |
No, it bypasses RLS |
Legacy service_role |
Long JWT starting eyJ... |
No | service_role |
No |
The catch: the two legacy keys look the same. Both are long eyJ... JWTs. To tell them apart, decode the middle part on your own machine. Don't paste keys into online decoders.
node -e 'console.log(JSON.parse(Buffer.from(process.argv[1].split(".")[1], "base64url").toString()))' "eyJhbGciOi..."
# { iss: 'supabase', ref: 'abcd1234', role: 'anon', ... } <- fine in the browser
# { iss: 'supabase', ref: 'abcd1234', role: 'service_role', ... } <- leaked secret: rotate now
If your front end contains a service_role JWT or an sb_secret_ key, stop reading and rotate it. Supabase's API keys guide has a rotation procedure. Supabase is also deprecating the legacy keys by the end of 2026, so it's a good moment to move to publishable and secret keys anyway.
Why you can't hide it (and don't need to)
People often ask whether they can keep the anon key out of the browser by moving it to .env, obfuscating it or putting a proxy in front. Putting it in .env is fine for configuration. But your front end has to send the key to talk to Supabase, so it ends up in your bundle and in every request in the network tab. Supabase's docs list "static or dynamic code analysis", reverse engineering and the browser's network inspector as things the publishable key does not protect against.
A proxy only helps if it adds a check of its own, such as requiring a signed-in session. Otherwise it's just the same open door at a different address. The protection has to live in the database.
What someone can do with your anon key
Anyone with your project URL and publishable or anon key can, as the anon role:
- Query any table or view that
anonhas a grant on, limited by RLS. A table with RLS off and ananongrant is fully readable, and often writable. - Call database functions over
rpc()ifanoncanexecutethem. RLS doesn't apply to functions, andsecurity definerfunctions run with their owner's rights. - Sign up for an account if email sign-ups are enabled, and become
authenticated. This one catches people out. A policy likefor select to authenticated using (true)doesn't mean "my users". It means anyone who registers. - Read files in public Storage buckets. Anyone can read public buckets by URL. Private buckets are governed by policies on
storage.objects. - Call your Edge Functions. The function itself must check who's calling. Supabase notes that with the new keys, the
verify_jwtsetting on its own doesn't authenticate a caller that only sends an API key.
None of this is a vulnerability while your grants and policies are right. All of it is one when they aren't.
How to check your project in 10 minutes
1. Find tables without RLS. In the SQL Editor:
select tablename from pg_tables
where schemaname = 'public' and rowsecurity = false;
2. See what anon and authenticated are granted:
select table_name, grantee, string_agg(privilege_type, ', ') as privileges
from information_schema.role_table_grants
where table_schema = 'public' and grantee in ('anon', 'authenticated')
group by table_name, grantee order by table_name;
3. Look for policies that are wide open:
select tablename, policyname, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public' and (qual = 'true' or with_check = 'true');
Each row should be data you intend to be public, such as published posts or a product catalogue. Anything with user data needs a policy tied to (select auth.uid()).
4. Test like an outsider. Signed out, with only your publishable key:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*&limit=5" \
-H "apikey: sb_publishable_..."
- A
42501permission error meansanonhas no grant, which is fine for private tables. - An empty list
[]means RLS is filtering everything out, which is fine. - Other people's rows mean you need to fix it now.
Then sign up a fresh test account and repeat the request with its session, to catch the "authenticated means anyone" problem from point 3.
5. Open Security Advisor in the Supabase Dashboard. It flags tables with RLS disabled in public and other common problems. Review each finding before you dismiss it.
Fixing what you find
For a table that holds per-user data:
alter table public.profiles enable row level security;
revoke all on table public.profiles from anon; -- signed-out visitors don't need it
grant select, update on table public.profiles to authenticated;
create policy "Users read own profile" on public.profiles
for select to authenticated using ((select auth.uid()) = id);
create policy "Users update own profile" on public.profiles
for update to authenticated
using ((select auth.uid()) = id) with check ((select auth.uid()) = id);
- Change
idtouser_id(or whatever your table uses), and test with two accounts. - Policies combine with OR, so check that no other permissive policy on the same table quietly undoes this one.
- From 30 October 2026, new tables in
publicdon't get any grants automatically, so new tables need explicitgrantstatements as well. See Supabase's 30 October GRANT change.
Full template, including public-read tables and functions: Supabase RLS and grants guide.
Keys that are never safe to expose
While you're looking through your bundle, check for keys that really are secret:
- Supabase
sb_secret_andservice_role; - Stripe
sk_live_/rk_live_; - OpenAI and Anthropic API keys;
- AWS access keys;
- GitHub tokens.
These often end up in the front end through a VITE_ or NEXT_PUBLIC_ variable. See Exposed API keys and Exposed .env and VITE_ secrets.
What StackSiren checks (and doesn't)
StackSiren downloads your page's HTML and its same-origin JavaScript files, the same way a browser does, and looks for key formats:
- A Supabase publishable key or legacy
anonJWT is reported as a low-severity reminder to verify RLS. That's expected, not an error. - A legacy
service_roleJWT (it decodes the JWT's role claim) or ansb_secret_key is reported as critical. Keys are always shown masked. - It never queries your database, so it can't tell whether your grants and policies are right. Use the checks above for that.
Run a free scan →. You get a free score and the full list of issues in under a minute.
FAQ
My AI builder says my Supabase key is exposed. Do I need to rotate it?
Not if it's the publishable key or a legacy JWT whose role is anon. Rotating it won't make you any safer, because the new one will be public too. Do rotate a service_role or secret key.
Is the anon key different from the publishable key?
They do the same job. anon is the legacy JWT version and sb_publishable_ is the newer short key. Supabase recommends moving to publishable and secret keys before the legacy keys are deprecated at the end of 2026.
Can someone delete my data with the anon key?
Only if anon (or authenticated, after signing up) has a delete grant on a table and RLS either is off or allows it. Check steps 1–3 above.
Does RLS slow my app down?
Simple policies are cheap. Supabase's RLS docs recommend writing (select auth.uid()) rather than auth.uid(), which lets Postgres cache the result, and adding indexes on the columns your policies filter on.
Fix guides for this topic
Platform checklists: Lovable · Bolt