Guide · Supabase Row Level Security

Free Supabase RLS checker, and how to check by hand

"RLS enabled" doesn't mean your data is private. Here's how to check what a stranger and another logged-in user can actually do, with the SQL to find the usual mistakes and to fix them.

Free · No signup · The check runs in your browser

Short answer

Check four things: every table in the public schema has RLS turned on, no rule on private data says using (true), no view or database function hands out rows your rules protect, and users can't edit columns like role or plan. Then test it the way an attacker would: as a stranger with your public key, and as a second user.

What RLS does

A Supabase app's website talks straight to the database through Supabase's API. To do that, it ships a public key (the anon key, called the publishable key in newer projects) inside your site's JavaScript. Anyone can open the browser's developer tools and copy it. That's by design: the key is meant to be public.

What keeps your data private is Row Level Security (RLS), a Postgres feature. When RLS is on for a table, every request is filtered through rules called policies. A policy says, for example, "a logged-in user can read a note only if user_id is their own id". When RLS is off, the public key can read, change and delete every row.

Why "RLS enabled" isn't enough

Apps built with AI tools like Lovable, Bolt or Replit usually do switch RLS on. The holes tend to be one step further in:

Supabase's free Security Advisor (in your project dashboard) catches several of these from your settings, and Supabase has been adding tools to test rules as a given user. A settings review alone won't tell you what a stranger or a second user can actually get, so a rule that's switched on but too loose can still look fine.

How to check, step by step

Run these in your project's SQL editor. In the Supabase dashboard it's SQL Editor; on Lovable Cloud it's under More, then Cloud, then SQL editor. They only read your database's structure, not your data.

1. Find tables with RLS off

Any table listed here as false is open to anyone holding your public key.

select c.relname as table_name, c.relrowsecurity as rls_on
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p')
order by rls_on, table_name;

2. Find rules that let everyone in

This lists every policy whose condition is simply true. Each one needs a decision: fine for a public product list, a leak for messages, orders or profiles.

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

3. Find views that ignore your rules

Views listed here run with their owner's rights, so RLS on the tables underneath doesn't apply to them.

select c.relname as view_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'v'
  and not coalesce(c.reloptions && array['security_invoker=true', 'security_invoker=on'], false);

4. Find full-rights functions anyone can call

A security definer function in public can be called through the API. If it returns rows, check that it only returns what the caller is allowed to see.

select p.proname as function_name
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef
  and has_function_privilege('anon', p.oid, 'execute');

5. Try it as a stranger

Settings tell you what should happen. A real request tells you what does. With your project URL and public key (both in your site's code anyway), ask for a table that should be private. Only do this on a project you own or manage.

curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*&limit=5" \
  -H "apikey: YOUR_PUBLIC_KEY"

An empty list [] is what you want for private data. Rows coming back means anyone on the internet can get them too.

6. Try it as another user

The stranger test misses a common leak: one signed-up user reading another user's rows. Sign up two test accounts, log in as the second one, and try to load the first one's data. You can also test from the SQL editor.

Newer Supabase dashboards let you pick a role next to the Run button in the SQL editor: choose authenticated and a user, then query the table. Without that option, run this in the SQL editor:

set role authenticated;
select set_config('request.jwt.claims', '{"sub": "SECOND-USER-UUID", "role": "authenticated"}', false);
select * from public.notes;  -- should show only the second user's rows

Afterwards, run reset role; in the same SQL editor tab, or just reload the editor.

Rather not run all of this by hand? The free Lockstamp check finds these patterns and tries the attacks as a stranger and as a second user, on a private copy in your browser.

Run the free check

How to fix what you find

The usual fix is to turn RLS on and give each private table owner-only rules. This example is for a notes table with a user_id column. Change the table and column names to match yours, and read it before you run it.

alter table public.notes enable row level security;

-- Drop any rule that lets everyone in, for example:
drop policy if exists "Enable read access for all users" on public.notes;

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

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

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

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

For the other holes from the checks above:

Make a view follow the caller's rules

alter view public.my_view set (security_invoker = true);

Stop strangers calling a full-rights function

revoke execute on function public.get_all_emails() from anon, authenticated, public;

If logged-in users need that function, keep their access and change it so it only returns the caller's own rows (for example where user_id = auth.uid()).

Let users edit only safe columns of their profile

revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url) on public.profiles to authenticated;

List every column your profile form saves, or those edits will start failing. If users create their own profile row, they could set role there too, so give it a default and limit the insert grant the same way.

After any change, log in as a normal user and click through your app's main features. A page that goes empty usually means a missing select rule. Our RLS not working guide covers those cases.

What no checker can tell you

A checker can show that a stranger can read a table. It can't know whether you meant that table to be public. Product listings and public reviews often should be readable by everyone; messages and orders should not. In rebuilds from public migration files of 27 projects, our attacks worked in 14, and most of those results still needed a person to decide whether the data was meant to be public.

Database rules are also only one part of an app. Keys pasted into front-end code, storage buckets, server functions and login settings need their own review, and no tool can promise an app has no other weaknesses.

FAQ

Questions about checking RLS

Isn't Supabase's Security Advisor enough?

It's a good first step and it's free. It reviews your settings and flags known patterns, like a table with RLS off. It doesn't try requests as a stranger or as another user, so a rule that is switched on but too loose can still pass. Use both.

Will turning on RLS break my app?

It can, for a moment. With RLS on and no rules, the table returns nothing, so pages that read it go empty until you add the policies. Add the enable statement and the policies together, then click through your app while logged in as a normal user.

Does the service_role key respect RLS?

No. The service_role key (called the secret key in newer projects) skips RLS completely. That's why it must only ever live on a server, never in your website's code. If it's in your front end, treat it as leaked and rotate it.

Is the Lockstamp check really free?

Yes. There's no signup, and it runs in your browser tab. The paid part is optional: if you'd rather not apply the fix yourself, you can hire Mark M. through Upwork to do it.

See which attacks actually work.

Free, about a minute, and nothing leaves your browser.

Run the free check