AppPulse

Guides

Supabase RLS checker: find tables anyone can read (free)

Find Supabase tables the anon key can read. Run two SQL queries and a logged-out GET, which is the same kind of read the free scan makes.

The anon key in a Supabase client is public. Row-level security is what stops that key from reading every row. A table with RLS off, and a grant that lets anon select, is readable by anyone who has the key. They already have it if your app loads in a browser.

The SQL below is the direct check. The DevTools request is the outside view, and it is the same kind of read the free scan performs. If you also found a service_role key in the bundle, rotate that first. The service role bypasses RLS, so policies do not contain it.

SQL: is RLS on?

In the Supabase SQL editor, run this as the project owner. It only reads catalog tables. It does not change data.

RLS flags for every ordinary table in public
select
  c.relname as table_name,
  c.relrowsecurity as rls_on,
  c.relforcerowsecurity as rls_forced
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
  and c.relkind = 'r'
order by 1;
Policies on those tables
select tablename, policyname, roles, cmd, qual
from pg_policies
where schemaname = 'public'
order by tablename, policyname;

A policy for anon or public with qual of true and command SELECT means anyone with the anon key can read that table. That can be intentional for a public changelog. It is wrong for profiles, orders, or messages. An empty policy list plus rls_on true means anonymous reads are denied.

DevTools: the same read a stranger can make

Log out of the app first. This uses the anon key only. Do not put a service_role key in the console.

Logged-out GET, one row max. Replace the URL, key, and table names.
const url = "https://YOUR_PROJECT.supabase.co";
const anon = "YOUR_ANON_KEY";
const tables = ["profiles", "orders"];

for (const table of tables) {
  const res = await fetch(
    `${url}/rest/v1/${table}?select=*&limit=1`,
    {
      headers: {
        apikey: anon,
        Authorization: `Bearer ${anon}`,
        Accept: "application/json",
        Prefer: "count=exact",
      },
    },
  );
  console.log(table, res.status, res.headers.get("content-range"), await res.text());
}

Lock a private table

Replace your_table and user_id with the real names.
alter table public.your_table enable row level security;

create policy "own rows" on public.your_table
  for select
  to authenticated
  using (auth.uid() = user_id);

Enabling RLS does not delete rows. Existing anon queries against that table start failing until a policy allows them. Repeat for insert, update, and delete if the client needs those, with the same auth.uid() check. Then rerun the logged-out GET and expect 401 or 403.

Prompt for Lovable, Bolt, Cursor, or v0
Enable row level security on every table in the public schema that stores user data.
Do not add a policy that lets the anon or public role select every row.
For each private table, add select, insert, update, and delete policies so a signed-in user can touch only rows where auth.uid() equals that table's user id column.
Leave the anon key in the browser. Do not add the service_role key to client code.

What the free scan actually checks

If the homepage or one of up to six linked scripts contains a Supabase anon JWT, the scan sends read-only GETs. It does not insert, update, or delete. First it requests the OpenAPI listing at /rest/v1/. Then it requests ?select=*&limit=1 for up to 12 tables named there, skipping RPC paths. A service_role key in the bundle is reported on its own and is not used for these requests.

HTTP 200 with rows is a high finding. HTTP 200 with an empty array is a medium finding, because the read was allowed. HTTP 401 or 403 produces no RLS finding for that table. If the listing returns no tables, the scan says it could not confirm RLS. It does not read pg_class, so it cannot see the RLS checkbox or the policy text. Use the SQL above for that. The Lovable key test covers whether the bundle also contains a service key.

Questions

Does enabling RLS delete any rows?

No. Enable row level security changes who can read and write. It does not drop data. With no policy, the anon and authenticated roles lose access. The table owner and the service role still bypass RLS.

Does the free scan prove every table has RLS on?

No. When a Supabase anon key is in the public page or a linked script, the scan sends read-only GETs for up to 12 tables from the OpenAPI list. HTTP 200 means that anon key could read the table. It does not open the SQL editor or read policy text.

Why would a table with RLS off still return 401?

RLS is not the only gate. If the anon role has no GRANT, PostgREST can still refuse the request. A 401 or 403 means this read was denied. It does not by itself prove a policy is correct.

Other guides

Run it against your live URL

The free scan reads the public site only. No account, no writes, and no use of a service key if one is sitting in the bundle.

Scan your URL