Skip to content
StackSiren

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:

  1. Query any table or view that anon has a grant on, limited by RLS. A table with RLS off and an anon grant is fully readable, and often writable.
  2. Call database functions over rpc() if anon can execute them. RLS doesn't apply to functions, and security definer functions run with their owner's rights.
  3. Sign up for an account if email sign-ups are enabled, and become authenticated. This one catches people out. A policy like for select to authenticated using (true) doesn't mean "my users". It means anyone who registers.
  4. Read files in public Storage buckets. Anyone can read public buckets by URL. Private buckets are governed by policies on storage.objects.
  5. Call your Edge Functions. The function itself must check who's calling. Supabase notes that with the new keys, the verify_jwt setting 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 42501 permission error means anon has 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 id to user_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 public don't get any grants automatically, so new tables need explicit grant statements 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_ and service_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 anon JWT is reported as a low-severity reminder to verify RLS. That's expected, not an error.
  • A legacy service_role JWT (it decodes the JWT's role claim) or an sb_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.

Platform notes: Lovable · Bolt

Check your app now

Free passive scan, results in under a minute.

Run a free scan

Fix guides for this topic

Platform checklists: Lovable · Bolt

Related posts

Sources