How to check3 min readUpdated 2026-08-03

How to check if your Supabase RLS actually works

Checking that a policy exists is not the same as checking that it denies anything. Test it the way an attacker would.


Supabase gives your browser a key and lets it query the database directly. Row Level Security is what decides which rows come back. Get it wrong and the answer is: all of them, to anyone.

The two-minute test

Query your own database as a stranger

  1. 1Open your live app, press F12, Network tab, reload the page.
  2. 2Click any request to *.supabase.co and copy the apikey header value and the project URL.
  3. 3List every table the key can see by requesting /rest/v1/ — Supabase returns an OpenAPI document naming them.
  4. 4For each interesting table, run the query below.

List the tables, then read one

# what tables can this key see?
curl -s "https://YOUR-PROJECT.supabase.co/rest/v1/" \
  -H "apikey: YOUR_ANON_KEY" | head -c 2000

# can an anonymous caller read rows?
curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*&limit=3" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"

Reading the result

  • [] — the table is protected. RLS is on and no policy grants anonymous reads.
  • Rows of real data — the table is public to the internet. Fix before anything else.
  • {"message":"permission denied for table ..."} — RLS is on and denying at the role level. Also fine.
  • Something about the relation not existing — wrong table name, not a security result.

Common ways RLS ends up not working

  • RLS was never enabled on the table — policies are ignored entirely until you ALTER TABLE ... ENABLE ROW LEVEL SECURITY.
  • A policy with USING (true), often added to 'make it work' during development and never revisited.
  • A policy on SELECT only, leaving INSERT, UPDATE and DELETE unrestricted.
  • Rules based on a role stored in user metadata the client can edit — the user can promote themselves.
  • Views that bypass the underlying table's policies.
Paste this into your AI coding tool
Audit Row Level Security across my whole Supabase project.

For every table in the public schema:
1. Report whether RLS is enabled.
2. List its policies, with the full USING and WITH CHECK clauses.
3. Flag any policy that is unconditional (USING true) or that depends on a
   value the client controls, such as a role stored in editable user
   metadata.
4. Confirm coverage for all four commands — SELECT, INSERT, UPDATE,
   DELETE — not just SELECT.

Then fix them: enable RLS everywhere, replace unconditional policies with
ownership checks written as USING ((select auth.uid()) = user_id), and for
tables no browser should touch, enable RLS with no policy so they deny by
default.

Finish with a table: table name, RLS on/off, policies per command, and
whether an anonymous user can read it.

Common questions

How do I know if Row Level Security is actually working?
Query the table as an anonymous user with your anon key and look at what comes back. An empty array means the table is protected; rows of real data mean it is public to the internet. Checking that a policy exists proves nothing — a policy written USING (true) matches every row for every caller and still reports as protected.
Is the Supabase anon key safe to have in my frontend?
Yes — the anon key is designed to sit in your browser bundle. It is safe only because Row Level Security decides which rows it can reach. With RLS off or written USING (true), that key reads your whole database.
Is enabling RLS enough on its own?
Enabling it is the first step, but a table can still be open: a policy on SELECT only leaves INSERT, UPDATE and DELETE unrestricted, rules based on a role stored in editable user metadata let the user promote themselves, and views can bypass the underlying table's policies.
My queries got slow after turning RLS on. What now?
Write (select auth.uid()) rather than bare auth.uid() in your policies. The subselect form is evaluated once per query instead of once per row, which on a large table is the difference between a fast query and a timeout — and that timeout is a common reason people disable RLS 'temporarily'.

This is one check out of 40+

Paste your site address and we run the whole list from the outside — leaked keys, open databases, unprotected pages — then hand you one prompt that fixes what we find. Free, about 30 seconds, no signup.

Check my site — free