Abbi Labs

Guides & tools

Supabase RLS policy checker (free, runs in your browser)

By Pritesh Ghodge, security engineer · Last updated 1 November 2026

"RLS enabled" isn't the same as protected. A policy like with check (true) on INSERT, or using (true) on UPDATE, keeps RLS switched on while letting anyone in that role write any row. Outside scanners (including ours) can only safely test reads. This checker reads your policies directly, so it also catches the write holes. Two steps, about a minute, no sign-up.

Step 1: run this in the Supabase SQL editor

Read-only. It reads catalog metadata (table names, RLS flags, column names, policies and grants), never rows from your tables.

-- Abbi Labs Supabase RLS policy checker (read-only; reads catalog metadata only, never row data)
with sch as (
  select n.oid, n.nspname from pg_namespace n
  where n.nspname not in ('pg_catalog','information_schema','pg_toast','auth','storage','realtime','_realtime',
    'extensions','graphql','graphql_public','pgsodium','pgsodium_masks','vault','supabase_functions',
    'supabase_migrations','net','cron','pgbouncer','_analytics','pgmq','topology','tiger','tiger_data')
    and n.nspname not like 'pg\_%'
)
select json_build_object(
  'tool', 'abbilabs-rls-checker-v1',
  'tables', coalesce((
    select json_agg(json_build_object(
      'schema', s.nspname, 'table', c.relname, 'kind', c.relkind::text,
      'rls', c.relrowsecurity, 'force_rls', c.relforcerowsecurity,
      'security_invoker', coalesce(c.reloptions::text[] && array['security_invoker=true','security_invoker=on','security_invoker=1'], false),
      'columns', (select coalesce(json_agg(a.attname order by a.attnum), '[]'::json)
                  from pg_attribute a where a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped)
    ) order by s.nspname, c.relname)
    from pg_class c join sch s on s.oid = c.relnamespace
    where c.relkind in ('r','p','v','m')
  ), '[]'::json),
  'policies', coalesce((
    select json_agg(json_build_object(
      'schema', p.schemaname, 'table', p.tablename, 'policy', p.policyname, 'permissive', p.permissive,
      'roles', p.roles, 'cmd', p.cmd, 'qual', p.qual, 'with_check', p.with_check
    ) order by p.schemaname, p.tablename, p.policyname)
    from pg_policies p where p.schemaname in (select nspname from sch)
  ), '[]'::json),
  'grants', coalesce((
    select json_agg(json_build_object(
      'schema', g.table_schema, 'table', g.table_name, 'grantee', g.grantee, 'privilege', g.privilege_type
    ) order by g.table_schema, g.table_name, g.grantee, g.privilege_type)
    from information_schema.role_table_grants g
    where g.grantee in ('anon','authenticated','PUBLIC') and g.table_schema in (select nspname from sch)
  ), '[]'::json)
) as rls_report;

Step 2: paste the result (the single rls_report cell)

Runs entirely in your browser. The JSON you paste is analysed on this page and never sent to Abbi Labs or anyone else. No sign-up.

What it flags

If a role has no grant for the operation a risky policy covers, the finding is lowered one level and says so: the Data API can't use it yet, but it opens the moment someone adds the GRANT, for example while fixing a 42501 permission denied error.

How Postgres RLS actually decides

Limits (read before trusting a clean result)

Now check what your live app actually exposes

The free Abbi Labs quick check finds your Supabase URL and public key in your site's JavaScript and tests whether common tables return rows to a signed-out visitor. It also looks for secret keys in your bundles, exposed .env and .git files, and missing security headers. It tests reads only, never writes, which is why this checker exists for INSERT and UPDATE policies. No sign-up.

Run the free live scan

FAQ

Does the checker upload my schema or data?

No. The SQL reads only catalog metadata (table and column names, RLS flags, policy expressions and grants), never rows. The JSON you paste is analysed by JavaScript on this page; it isn't sent to Abbi Labs or anyone else.

Is the SQL safe to run on production?

Yes. It's a single SELECT over pg_class, pg_attribute, pg_policies and information_schema.role_table_grants. It doesn't write, lock tables or read table contents.

Why is a policy with no WITH CHECK on INSERT reported as 'allows nothing'?

INSERT policies use only WITH CHECK. A permissive INSERT policy created without one doesn't let any row through (we verified this on Postgres 17), so it isn't a hole; it's usually a mistake that blocks inserts. The hole is WITH CHECK (true).

What can't it see?

The body of functions called from policies (including security definer helpers like is_admin()), your app's server code and edge functions, data in the rows, and anything the service_role key does (it bypasses RLS). A clean result means no obvious policy holes, not that the app is secure.

Related guides

Weekly AI-app security checklist

One short email a week: a practical check for apps built with Supabase, Next.js and AI tools (RLS, leaked keys, headers and more). Unsubscribe anytime.