What the Supabase RLS checker looks at
Row level security (RLS) is the Postgres feature that decides, row by row, what each request may read or write. In Supabase it is the only thing standing between your anon key, which ships in every visitor's browser, and your data. The query above reads Postgres's own catalog for the schemas your Data API exposes and returns one JSON value: tables, policies, grants, views and security definer functions. No row of your data is in it. This page reads that value in your browser and runs these checks.
| Check | Why it matters | Usual severity |
|---|---|---|
| RLS off on a table the API can reach | Postgres then checks only privileges. If anon has them, anyone with your anon key reads and writes every row. | Critical |
A policy whose condition is true for anon or public | Permissive policies add up: one open policy opens the table even when another checks the owner. | Critical to medium |
A policy for authenticated that never compares a column to auth.uid() | Anyone who signs up gets the authenticated role, so every stranger with an account passes. | High |
Users who can update their own role, is_admin, plan or credits | An update policy says whose row it is, not which columns may change. | High |
A policy that reads user_metadata | Users can edit their own user_metadata from the browser. | High |
A view without security_invoker | A view runs as its owner, usually postgres, and skips the RLS of the tables it reads. | High |
A security definer function that anon can execute | It runs with its owner's rights and is served at /rest/v1/rpc/. | High |
An update policy without with check | Postgres reuses the USING condition, so this is a readability issue more than a hole. | Low |
It also catches partitions with RLS off (a partition does not inherit its parent's policies, so naming it directly skips them), materialized views the API can read, and views that hand auth.users to the API.
How to read the result
The verdict comes first, colored by the worst finding. Critical means a stranger holding only your anon key can reach data today. High means strangers, or anyone who signs up, can reach data they should not see. Medium covers things to check, like a table anyone can read that may be public on purpose. Each finding names the object, says what a stranger can do and gives the SQL that fixes it. Copy all fixes puts them in one script, most severe first, ready for the SQL editor or for your coding agent.
The checker reads grants as well as policies. A table with RLS off that anon and authenticated have no privilege on is reported as low, because nothing reaches it through the API today. That case is more common since 30 October 2026: Supabase stopped granting API access to new tables in public automatically, while older tables kept their grants. Watch the usual quick fix for the resulting 42501 permission denied error: a grant ... to anon on a table with RLS off opens it to everyone.
How to fix what it finds
Every finding comes with its own fix, written for your table and column names. These are the four patterns behind most of them.
Turn RLS on and add owner policies
alter table public.invoices enable row level security;
create policy "invoices: owner reads" on public.invoices
for select to authenticated
using ((select auth.uid()) = user_id);With RLS on and no policy, the table is closed to both API roles, a safe state while you write the policies your app needs. Writing (select auth.uid()) instead of auth.uid() lets Postgres evaluate it once per query rather than once per row, as Supabase recommends.
Replace policies that let everyone in
drop policy "Enable read access for all users" on public.messages;
create policy "messages: parties read" on public.messages
for select to authenticated
using ((select auth.uid()) in (sender_id, recipient_id));Keep using (true) only where every row and every column is meant to be public, like published posts. For a public form such as a waitlist, an open insert policy is a choice: the checker then suggests closing reads and limiting the columns anon may fill.
Stop users from promoting themselves
revoke update on public.profiles from authenticated;
grant update (username, avatar_url) on public.profiles to authenticated;Signed-in users can then change only the listed columns. role changes only from your server with the service role key, or from the SQL editor.
Close views and functions that skip RLS
alter view public.project_stats set (security_invoker = true);
revoke execute on function public.get_user_id_by_email(text) from public, anon;Revoke from both roles. Supabase's default privileges grant execute to anon directly, so revoking from public alone can leave the function callable. The Supabase RLS guide has policies for teams, shared rows and admin roles.
What a static check cannot tell you
This checker reads what your database says. It does not send a single request to your project, so be clear about what it misses.
- Inside functions. A policy that calls
is_team_member(team_id)is only as strict as that function, and the checker cannot read its body. - Your data. A correct policy still leaks if the app writes the wrong id into the owner column.
- Storage, Realtime and Edge Functions. Buckets have their own policies on
storage.objects. The Supabase storage guide covers them. - Anonymous sign-ins. If you turned them on, those visitors get the
authenticatedrole too, so a policy that only checks for a signed-in user lets them in without an email. - Pasted SQL. A static read of migrations cannot see dashboard changes, later migrations or the grants your project really has. Run the query for the real state.
Supabase's own Security Advisor, under Advisors in the dashboard, flags tables with RLS off, always-true policies, security definer views and functions anon can call. Use both: the advisor runs on every project, and this checker explains each finding in plain words, catches policies that trust any signed-up user and columns users can promote themselves with, and reads migrations before they ship. For the rest of your stack, start with the Supabase security guide.
Questions
How do I check if RLS is enabled on my Supabase tables?
Run the read-only query on this page in the SQL editor and paste the result: every table with RLS off is reported with the SQL that turns it on, as critical when the anon role can reach it. The Security Advisor in the Supabase dashboard flags them too. Check partitions as well, since they do not inherit RLS from their parent.
What does a Supabase RLS policy with using (true) do?
It lets every role the policy applies to read every row, or write any row for insert, update and delete policies. Without a to clause the policy applies to public, which includes anon, so anyone with your anon key passes. Keep it only for data meant to be public.
Is a Supabase RLS policy for authenticated users enough?
Not on its own. Anyone can sign up to your app and get the authenticated role, so a policy like to authenticated using (true) lets every stranger with an account read every row. Compare a column to auth.uid() so each user reaches only their own rows. The IDOR guide explains the pattern.
Does the service role key bypass Supabase RLS policies?
Yes. The service_role role has the BYPASSRLS attribute, so RLS policies never apply to it. That is why the service role key belongs on your server only, and why the anon key guide treats a leaked service role key as an emergency.
Why does my Supabase query return an empty array after enabling RLS?
With RLS on, a request sees only the rows a policy allows, and with no policy at all it sees none. Add a select policy for the role your app uses. If you get a 42501 permission denied error instead, the role has no grant on the table, which is a separate layer from RLS.
Is this Supabase RLS checker safe to run on a production database?
Yes. The query only reads Postgres's catalog and changes nothing, and its result holds table names, policy text and grants, not your rows. The analysis runs in your browser: nothing you paste is sent to Keybrum.