You run it, we never connect
The Supabase catalogue audit
A free xlogs scan asks your database for rows as a stranger, which is the strongest evidence there is about what is exposed and is also, necessarily, limited to what a stranger can reach. The 6 questions below are the ones only the database itself can answer. Running them needs a credential, so you run them, not us.
Why this is safe to run
Every statement reads from PostgreSQL's system catalogues or from storage bucket metadata. Not one selects from an application table, so there is nothing in the output but names and settings: no rows, no user data, nothing of anyone's to leak. That is a property of the SQL rather than a promise about it, and test/repo-depth.test.mjs proves it on every commit by parsing every source the query reads and failing if any is not a catalogue.
Verified at render: 7 sources read, all of them system catalogues (information_schema.role_table_grants, pg_catalog.pg_class, pg_catalog.pg_namespace, pg_catalog.pg_policies, pg_catalog.pg_policy, pg_catalog.pg_proc, storage.buckets), and no statement that writes.
- xlogs never connects. There is no field on this site for a database URL or a service key, and there is nowhere for one to be stored.
- The output stays yours. Read it yourself or paste it to your own coding agent. Nothing comes back to us.
- Findings from it are free, like every other finding. This is not a paid tier.
What each block answers
1. Tables without row level security
Anything here is readable by whoever holds the anon key, which in a browser app is everybody.
2. Every policy and its real predicate
Look for a predicate of just "true", and for predicates that check only that someone is signed in rather than which someone.
3. Row level security on, but no policy
Denies everyone. Safe, but usually an accident: this shape breaks an app rather than exposing it.
4. Public storage buckets
Bucket metadata, not the files. A public bucket is a deliberate choice worth confirming.
5. SECURITY DEFINER functions
Legitimate and common, and also how a policy gets bypassed. Each one is worth being able to explain.
6. Direct grants to anon and authenticated
Table privileges sit underneath row level security. A grant here is what makes a policy the only thing in the way.
The query
Paste this into the SQL editor in your Supabase dashboard, or hand it to the coding agent that built the app. It is one run and it changes nothing.
-- xlogs catalogue audit. READ-ONLY. Reads PostgreSQL system catalogues only.
-- It selects NOTHING from any application table, so running it cannot expose your data.
-- Paste into the Supabase SQL editor (or your own agent) and send the output back to yourself.
-- 1. Tables without row level security. Anything listed here is readable by whoever holds the
-- anon key, which in a browser app is everybody.
select
n.nspname as schema,
c.relname as table,
c.relrowsecurity as rls_enabled,
c.relforcerowsecurity as rls_forced
from pg_catalog.pg_class c
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'r'
and n.nspname not in ('pg_catalog','information_schema','auth','storage','extensions','graphql','realtime','vault','supabase_migrations')
order by c.relrowsecurity asc, n.nspname, c.relname;
-- 2. Every policy, with its actual predicate. Look for "true" as a whole predicate, and for
-- predicates that check only that SOMEONE is signed in rather than WHICH someone.
select
schemaname as schema,
tablename as table,
policyname as policy,
roles,
cmd as command,
qual as using_expression,
with_check as with_check_expression
from pg_catalog.pg_policies
where schemaname not in ('pg_catalog','information_schema','auth','storage','extensions','graphql','realtime','vault','supabase_migrations')
order by schemaname, tablename, policyname;
-- 3. Tables with row level security ON but NO policy. This denies everyone, which is safe but is
-- usually an accident: it is the shape that breaks an app rather than exposes it.
select n.nspname as schema, c.relname as table
from pg_catalog.pg_class c
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'r'
and c.relrowsecurity
and n.nspname not in ('pg_catalog','information_schema','auth','storage','extensions','graphql','realtime','vault','supabase_migrations')
and not exists (select 1 from pg_catalog.pg_policy p where p.polrelid = c.oid)
order by 1, 2;
-- 4. Storage buckets marked public. This is bucket METADATA, not the files in them.
select id, name, public, file_size_limit, allowed_mime_types
from storage.buckets
order by public desc, name;
-- 5. Functions that run as their owner rather than the caller. SECURITY DEFINER is legitimate and
-- common; it is also how a policy gets bypassed, so each one is worth being able to explain.
select
n.nspname as schema,
p.proname as function,
p.prosecdef as security_definer
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where p.prosecdef
and n.nspname not in ('pg_catalog','information_schema','extensions','graphql','realtime','vault')
order by 1, 2;
-- 6. What the anon and authenticated roles have been granted directly. Table privileges sit
-- UNDERNEATH row level security: a grant here is what makes a policy the only thing in the way.
select table_schema as schema, table_name as table, grantee, privilege_type
from information_schema.role_table_grants
where grantee in ('anon','authenticated')
and table_schema not in ('pg_catalog','information_schema','auth','storage','extensions','graphql','realtime','vault','supabase_migrations')
order by grantee, table_schema, table_name, privilege_type;
Reading the result
- A table in block 1 with row level security off is readable by whoever holds your anon key, which in a browser app is everybody. That is the row to act on first.
- A policy in block 2 whose predicate is just
truelets every row through. For a table of countries or price plans that is correct. For anything about a person it is the line that exposes them. - A predicate that only checks somebody is signed in is not authorisation when rows belong to individual users: every signed-in user can then read every other user's rows.
- Blocks 5 and 6 are context, not alarms. A function that runs as its owner and a direct grant to
anonare both legitimate and both are how a policy gets bypassed, so they are worth being able to explain rather than worth panicking about.
The repository scanner reads the other half of this from your migrations, and a free scan tests the live half by asking. Scan a repository · Scan a deployment · Methodology
