29 October 2026 · 7 min read · BlitzHash Team · Supabase
Supabase RLS check: a 30-minute walkthrough for when "RLS is on" but data is still open
A Supabase RLS check answers one question: can a stranger, or another signed-in customer, read or change data that isn't theirs? The dashboard showing "RLS enabled" on every table is not the answer. Tables can have Row Level Security switched on and still be wide open because of a single rule that says true, a view that ignores the rules, or a database function that runs with admin rights. This walkthrough takes about 30 minutes, uses read-only queries you paste into the Supabase SQL Editor, and tells you exactly which of those holes you have.
If you'd rather hand it to someone, our Supabase RLS check does this review for you and returns a ranked fix list. If you want to try it yourself first, read on.
Why "RLS is on" can still mean "everyone can read it"
Row Level Security is a Postgres feature. When it's on for a table, every request through Supabase's API is filtered by the table's policies. Supabase's docs describe policies as an implicit WHERE clause added to every query.
Three things go wrong in AI-built apps again and again:
1. The policy lets everyone through. A policy written as USING (true) passes every row. Supabase's Security Advisor flags this as 0024_permissive_rls_policy. AI tools often write it as a placeholder to "make the error go away". 2. The data is reached another way. Views in Postgres run with their creator's permissions by default, so they can skip RLS (advisor 0010_security_definer_view). Functions marked SECURITY DEFINER also run as their owner, and are often callable by anyone through /rest/v1/rpc/ (advisors 0028 and 0029). 3. The rule trusts something the user controls. A policy that reads user_metadata can be bypassed, because users can edit their own metadata (advisor 0015_rls_references_user_metadata, rated ERROR).
Each of these leaves the dashboard looking healthy.
Before you start
- Open your project in the Supabase dashboard and go to SQL Editor.
- Every query below only reads the database catalogue. None of them change data. Still, run them on a staging copy if you have one.
- Keep a note open. For each step, write the table or function names that come back.
Lovable users with Lovable's built-in backend can run the same ideas through Lovable's database view and security scan; if you connected your own Supabase project, use its dashboard.
The 30-minute Supabase RLS check
Step 1 (3 minutes): Run the Security Advisor
In the dashboard, open Advisors → Security Advisor and run it. Write down every ERROR and WARN. Supabase's advisors are deterministic checks that ship with the platform, so this is your baseline. The steps below confirm the findings and catch the ones that need human judgement.
Step 2 (3 minutes): Find tables with RLS off
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 c.relrowsecurity, c.relname;Any row with rls_on = false is a table that, per Supabase's advisor docs, anyone with your project URL can read, edit and delete through the API. Fix these first: alter table public. enable row level security; and then add the policies the app needs. Note that turning RLS on with no policies blocks all API access to that table, so test the app right after.
Step 3 (5 minutes): Find policies that say "everyone"
select tablename, policyname, roles, cmd, qual, with_check
from pg_policies
where schemaname in ('public', 'storage')
and (qual = 'true' or with_check = 'true')
order by tablename;For each result, ask whether that table is meant to be public. A product list or published blog posts: fine. Orders, messages, invoices, profiles with emails: not fine. Replace the true with a real condition, usually (select auth.uid()) = user_id.
Step 4 (4 minutes): Check who each policy applies to
Look at the roles column from Step 3, or run it for all policies by removing the and (...) line. When a policy is created without a TO clause, Postgres applies it to public, which means every role, including signed-out visitors (anon). A rule intended for "logged-in users" that shows {public} here also applies to strangers.
Also note policies on authenticated. Supabase's anonymous sign-ins use the same authenticated role as permanent users, so if you enabled anonymous sign-ins, an "any logged-in user" rule includes anyone who clicked "continue as guest".
Step 5 (4 minutes): Read USING versus WITH CHECK
Two parts of a policy do different jobs:
- USING decides which existing rows you can see, update or delete.
- WITH CHECK decides which new or changed rows you're allowed to write.
The classic gap is an INSERT policy with WITH CHECK (true). That lets a signed-in user create a row with somebody else's user_id, such as a message that appears to come from another customer, or an order assigned to a different account. For inserts, the check should tie the new row to the caller, for example with check ((select auth.uid()) = user_id).
Step 6 (3 minutes): Find views that skip RLS
select c.relname as view_name, c.reloptions
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind = 'v';Any view whose reloptions doesn't include security_invoker=on (or true) runs as its creator and can ignore the rules on the tables underneath. Recreate it with (security_invoker = on), as Supabase's advisor docs recommend. Views that read from auth.users deserve extra attention; Supabase rates that pattern ERROR (0002_auth_users_exposed).
Step 7 (4 minutes): Find admin-powered functions anyone can call
select p.proname as function_name,
pg_get_function_identity_arguments(p.oid) as args,
has_function_privilege('anon', p.oid, 'execute') as strangers_can_call,
has_function_privilege('authenticated', p.oid, 'execute') as users_can_call
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
and p.prosecdef;These functions run with their owner's rights, which usually means they can read every row. Supabase notes that new functions in public are executable by anon and authenticated by default. For each one that shows true, either revoke access (revoke execute on function public.() from anon, authenticated, public;), switch it to security invoker, or confirm it's meant to be public and validates its input.
Step 8 (2 minutes): Find rules that trust user-editable data
select tablename, policyname
from pg_policies
where qual ilike '%user_metadata%'
or with_check ilike '%user_metadata%';Anything here can be bypassed by a user who edits their own metadata. Move roles into a table that only server code can write, and point the policy at that table.
Step 9 (2 minutes): Prove it as a stranger
Now test from the outside. Take your project URL and your publishable (or legacy anon) key, which are already public in your app, and request a table that should be private:
curl "https://.supabase.co/rest/v1/orders?select=*&limit=5" \
-H "apikey: "On older projects using the legacy anon key, also add -H "Authorization: Bearer ". An empty list [] for a private table is what you want. Rows coming back means strangers can read it.
Scoring your Supabase RLS check
| Finding | Severity | Fix by | |---|---|---| | RLS off on a table with customer data (Step 2) | Critical | Today | | true policy on a private table (Step 3) | Critical | Today | | Stranger curl returns private rows (Step 9) | Critical | Today | | Security-definer function callable by anon (Step 7) | High | This week | | View without security_invoker over private data (Step 6) | High | This week | | Policy reads user_metadata (Step 8) | High | This week | | Policy on {public} meant for signed-in users (Step 4) | Medium | This week | | INSERT with WITH CHECK (true) (Step 5) | Medium | Before launch |
After fixing, re-run the Security Advisor and repeat Step 9 plus a two-account test from inside the app. Static queries tell you what the rules say; only a real test tells you what a second customer sees. We explain that test step by step in is my AI-built app secure? The two-account test.
For background on why the public key isn't the problem, read is the Supabase anon key safe in the browser?. If you're building something with roles, such as staff, managers and owners, our post on building a CRM on Supabase shows a policy layout that holds up. And for a quick overall read, try the free launch-ready self-check.
Practical takeaways
- "RLS enabled" is a starting point. The rules, views and functions decide what's actually exposed.
- Search for
truepolicies and treat every one on a private table as a leak. - Views and
SECURITY DEFINERfunctions are the most common back doors. - Never base access on
user_metadata. - Finish with a stranger test and a two-account test. Queries alone don't prove privacy.
FAQ
Is the Supabase Security Advisor enough on its own?
It's an excellent first pass, and it catches many of the issues above. But it can't know which tables are meant to be public, so it flags some intentional settings and can't judge business rules. Use it as Step 1, not the whole check.
Will turning on RLS break my app?
It can, briefly. With RLS on and no policies, the API returns no rows for that table. Add the policies your screens need, then test sign-in, lists and saves straight away.
How often should I run a Supabase RLS check?
Before launch, after any change to tables, roles or sign-in, and whenever an AI tool has rewritten your database rules. AI builders sometimes loosen a policy to fix an error message, so re-check after big prompting sessions.
Do I need the service role key for these queries?
No. Run them in the dashboard's SQL Editor while signed in to your project. Never paste a secret or service role key into a browser app or a third-party tool to do this.
---
Not sure what your results mean, or found something in Steps 2, 3 or 9? Our Supabase RLS check reviews every table, policy, view and function and gives you a plain-English fix order. Book a free technical assessment and we'll tell you what a stranger can see today.
Sources
- Supabase: Row Level Security
- Supabase: Advisors (security lints)
- Supabase: Anonymous sign-ins
- PostgreSQL: CREATE POLICY
- PostgreSQL: pg_policies
