Skip to content
SealMyApp

Is my Supabase anon key safe to expose? (Yes — here's what actually protects your data)

Yes, the Supabase anon (publishable) key is meant to be public. What protects your data is Row Level Security. Here's how to check and fix it.

Updated 2026-10-16

Yes. The Supabase anon key (and its replacement, the sb_publishable_… key) is designed to sit in your frontend where anyone can see it. It is not what protects your data — Row Level Security (RLS) is — so the real question is whether every table it can reach has RLS switched on with sensible policies.

If you built your app with an AI builder and spotted the key in your browser's dev tools, that alone is not a breach. Below is what the key can and can't do, the one key that must never be public, and a five-minute check to confirm you're actually safe.

What the anon / publishable key is for

Supabase gives every project two kinds of API key:

KeyOld nameWho should see itWhat it can do
Publishable (sb_publishable_…)anon (a long JWT starting eyJ…)Everyone — it's in your frontendWhatever your RLS policies allow the anon role (or a signed-in user) to do
Secret (sb_secret_…)service_roleYour servers onlyEverything. It bypasses RLS entirely

Supabase's own API keys documentation lists publishable keys as safe to expose in web pages, mobile and desktop apps and source code, and says plainly that Row Level Security is what decides what that key can reach, so it should be on for every table before you deploy.

Think of the anon key as your app's address, not its front door key. Anyone can find the address. The locks are your RLS policies.

So where's the danger?

The danger is a table that the anon key can reach without a lock on it. That happens in three common ways:

  1. RLS is off on a table. Tables created with raw SQL or migrations (which AI builders often write) only get RLS if the SQL turns it on. With RLS off, anyone holding your public key can usually read — and sometimes change — every row.
  2. RLS is on, but a policy lets everyone through. A policy such as using (true) for anon makes a table world-readable. AI builders sometimes add this to "fix" a screen that shows no data.
  3. "Logged in" is treated as "allowed". A policy that only checks the user is authenticated lets every person who signs up read everyone's rows. Signing up is free, so this is barely better than public.

None of these are fixed by hiding the anon key. Even if you could hide it, it's sent in the apikey header of every request your app makes, so anyone can pick it up from the Network tab.

The key that must never be public

The service_role key, or a new-style sb_secret_… key, bypasses every RLS policy. Supabase's docs are blunt about it: a secret key bypasses every Row Level Security policy you have. If that key is in your JavaScript bundle, your RLS doesn't matter — anyone can read and write everything.

How to tell which key you're looking at:

  • Starts with sb_publishable_ → fine to expose.
  • Starts with sb_secret_ → server only. Supabase says these keys are rejected with a 401 when used from a browser, but a leaked one can still be used from a script, so treat it as compromised.
  • A long string starting eyJ → a legacy JWT. Paste the middle section (between the two dots) into a base64 decoder. If it contains "role":"anon" it's the public key. If it contains "role":"service_role" it must come out of your frontend and be rotated.

Five-minute check: is your anon key actually safe?

Only run these against a project you own. Everything here is read-only.

1. List tables and whether RLS is on. In the Supabase SQL editor:

select tablename, rowsecurity as rls_enabled
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

Any false is a table to fix today.

2. Look for policies that let everyone in.

select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
  and (qual = 'true' or with_check = 'true')
order by tablename;

A true on a SELECT policy is fine for genuinely public data (published blog posts, a product catalogue). It is not fine for profiles, orders, messages or anything with an email address in it.

3. Ask the API what a stranger sees. Replace the placeholders:

curl "https://YOUR_PROJECT_REF.supabase.co/rest/v1/profiles?select=*&limit=1" \
  -H "apikey: YOUR_ANON_OR_PUBLISHABLE_KEY"

(With a legacy eyJ… anon key, also add -H "Authorization: Bearer YOUR_ANON_KEY".)

  • A row comes back → anyone on the internet can read that table.
  • [] → nothing visible anonymously (or the table is empty — check).
  • A permission error → the anonymous role has no access at all. Good.

4. Open the Security Advisor. In the dashboard, go to Advisors → Security Advisor. Lints such as rls_disabled_in_public and policy_exists_rls_disabled flag exactly this problem.

The fix

Turn RLS on for each private table:

alter table public.profiles enable row level security;

Then add a policy that only lets people see their own rows:

create policy "profiles: read own"
  on public.profiles for select to authenticated
  using ((select auth.uid()) = id);

With RLS on and no policies, nothing is readable through the API with the public key — a safe starting point. Add policies to open exactly what each screen needs. Our Row Level Security guide has copy-paste policies for owner-only data, public-read/owner-write, teams, insert-only forms and Storage.

If your builder writes the SQL for you, paste this prompt and review the migration before it runs:

Check every table in the public schema. Enable Row Level Security on any table
where it is off. Replace any policy that uses "using (true)" on private data with
a policy that matches the owner column against (select auth.uid()). Never put the
service_role or sb_secret key in frontend code. Show me the SQL before running it
and list any tables you left publicly readable, with the reason.

If you found a service_role key in your frontend: remove it, move that code into a server-side function, then create a new secret key and disable the leaked one under Project Settings → API Keys. Supabase notes that creating new keys doesn't revoke old ones — you have to disable the legacy keys yourself.

FAQ

Should I hide my Supabase anon key in an environment variable?

You can keep it in an env var for convenience (for example VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY), but it still ends up in the browser bundle — that's how the browser talks to Supabase. Env vars don't make a public key private. RLS does.

Someone told me my anon key is "leaked". Do I need to rotate it?

Not because it's visible — it's meant to be. Rotate it only if you have another reason to. Spend the time checking RLS instead.

Is the publishable key safer than the old anon key?

Both are low-privilege public keys and both rely on RLS. The newer keys are easier to rotate independently, and Supabase's docs say the legacy anon/service_role keys are being deprecated, so it's worth switching when convenient.

Does the same apply to Firebase?

Yes — Firebase's web API key is also public by design and your data is protected by Security Rules. See our Firebase security rules guide.

My app is on Lovable Cloud, not my own Supabase. Does this apply?

Lovable Cloud is built on Supabase, so the same model applies. Lovable shows RLS policies under More → Cloud → Database → RLS policies, and its Security view flags database access problems.

Check it for free

Our free scan does the anonymous-visitor test above against your live app (after you prove you own it), reads at most one row to confirm a table is exposed, keeps nothing, and won't flag your public anon key as a problem. Run a free scan.

Sealed — a short weekly security note for vibe coders

One common security mistake in AI-built apps, how to fix it in your builder, and one useful tool. No spam; unsubscribe any time.