Home / Guides / Supabase anon key exposed? Check your RLS

Your Supabase anon key is public. That's fine. Missing RLS isn't.

If a scanner, a friend or an AI assistant told you "your Supabase key is exposed in your website's code", take a breath. That key is supposed to be there. What decides whether your users' data is safe is something else: Row Level Security, usually shortened to RLS.

The two kinds of Supabase key, in plain English

KeyWhere it belongsWhat it can do
Publishable key (sb_publishable_...), formerly the anon keyYour website or app. Public by design.Only what your RLS policies allow.
Secret key (sb_secret_...), formerly the service_role keyServers and Edge Functions only. Never the browser.Everything. It skips RLS completely.

Supabase is moving from the old names (anon and service_role, long keys that start with eyJ) to the new publishable and secret keys. Both kinds work while projects migrate, so you may see either in your code.

What actually goes wrong

Your Supabase project has a public web address that answers database requests. Anyone who has your publishable key can send requests to it directly, without using your app at all. If a table has RLS switched off, or a policy that lets everyone in, those requests get the data.

This is the most common serious problem I see in apps built with AI tools. The app looks fine, because the app only ever asks for the current user's rows. A stranger simply asks for all of them.

The five-minute test

  1. Read the Security Advisor. In the Supabase dashboard, open Advisors, then Security Advisor. Any warning that RLS is disabled on a public table is your first fix.
  2. Ask the database directly. On your own project only, run this in a terminal with your project's address and publishable key:
    curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*" \
      -H "apikey: YOUR_PUBLISHABLE_KEY"
    Swap profiles for each table name. If you get back rows that belong to other people, that table is public.
  3. Try it as a real user. Log in as user A, take the ID of something that belongs to user B, and try to open or edit it through the app. If it works, your policies are too loose.

The fix: switch RLS on and add policies

Here is the usual setup for a table where each row belongs to one user through a user_id column. Run it in the SQL editor and change the table name to match yours:

alter table public.notes enable row level security;

create policy "Users can read their own notes"
  on public.notes for select to authenticated
  using ( (select auth.uid()) = user_id );

create policy "Users can add their own notes"
  on public.notes for insert to authenticated
  with check ( (select auth.uid()) = user_id );

create policy "Users can edit their own notes"
  on public.notes for update to authenticated
  using ( (select auth.uid()) = user_id )
  with check ( (select auth.uid()) = user_id );

create policy "Users can delete their own notes"
  on public.notes for delete to authenticated
  using ( (select auth.uid()) = user_id );

Writing (select auth.uid()) instead of plain auth.uid() is a performance tip from Supabase's own docs: the user ID is worked out once per query instead of once per row. An index on user_id helps for the same reason.

Five traps that catch people after they "fixed" it

RLS on, but nothing shows up

With RLS switched on and no policies, the database returns nothing. That is the safe default, not a bug. Add a policy for each thing your users need to do.

A policy that says using (true)

AI tools often write this to make an error go away. It means "everyone may read every row". Combined with to authenticated, it means any logged-in user may read every row, which on an app with open sign-up is almost the same thing.

Tables created with SQL

Tables made in the Table Editor usually have RLS switched on for you. Tables created with SQL, including the ones an AI tool created for you, often do not. Check every table, not just the first one.

Views skip RLS

A view runs with the permissions of whoever created it, so by default it can show rows the user should not see. On Postgres 15 and newer, create views with with (security_invoker = true) so they respect the caller's policies.

Storage has its own rules

File storage is protected separately from your tables. A locked-down database with a public bucket of invoices is still a leak. Check each bucket and its policies too.

If your secret or service_role key was exposed

This one is urgent, because that key skips every policy. Rotate it in the Supabase dashboard under the API keys settings, then move the code that used it into an Edge Function or another server. With the new secret keys you can create a replacement and delete the leaked one. With the legacy service_role key, rotating can mean changing the project's JWT secret, which also replaces the anon key and signs your users out, so plan a short window for it.

Quick answers

Is it safe to expose the Supabase anon key?

Yes. The anon key, and the newer publishable key that replaces it, is designed to be used in the browser. It only allows what your Row Level Security policies allow. The danger is tables with RLS switched off or with policies that let everyone in.

What happens if my Supabase service_role or secret key is exposed?

Treat it as an emergency. That key skips Row Level Security completely, so whoever has it can read and change everything. Rotate it in the Supabase dashboard straight away and move any code that used it onto a server.

Why does my app show no data after I turn on RLS?

With RLS on and no policies, Postgres returns nothing by default. That is expected. Add a policy for each action your users need, such as reading and editing their own rows.

Not sure if this applies to your app?

Paste your link and I'll check what anyone on the internet can already see. Free, plain English, no login.

Check my app for free