Concepto505/supabase-audit

Eleven Supabase/Postgres production failure modes, found by one SQL file you paste into the SQL Editor. No install, no credentials shared.

0

0 commits

updated Sep 20, 2026

See the code
postgres
rls
row-level-security
security-audit
supabase

See what people are saying (1)

README

supabase-audit

One SQL file. Paste it into the Supabase SQL Editor, hit Run, get a list of the things that are about to bite you — worst first.

No install, no npm package, no connection string handed to a stranger, no account anywhere. It only SELECTs from system catalogs: it reads none of your table data, writes nothing, and sends nothing anywhere. It is one file so that you can read it before you run it.

curl -O https://raw.githubusercontent.com/Concepto505/supabase-audit/main/audit.sql

What it checks

#CheckWhy it matters
1Tables exposed to the API with RLS disabledThe anon key ships in your frontend bundle. Anyone can read every row.
2RLS on, zero policiesEvery client query returns an empty set. The classic "works in the SQL editor, returns nothing in the app".
3Policies open to anon with an always-true conditionA leak, unless that data is deliberately public.
4SECURITY DEFINER functions executable by PUBLICPostgres grants EXECUTE to PUBLIC by default. These are born open to the anon key.
5SECURITY DEFINER without a pinned search_pathPrivilege escalation: a caller can shadow a function it calls.
6Views that run as their ownerRLS on the underlying tables is not applied to the caller. Often correct — flagged for a decision, not as a bug.
7Secret-shaped columns on API-reachable tablesA leaked row hands over a live credential.
8auth.uid() re-evaluated per rowThe single biggest RLS slowdown. 20 ms vs 20 s on a large table.
9Policies that query their own tableinfinite recursion detected in policy for relation.
10Foreign keys with no indexEvery join and every cascading delete scans the whole table.
11Extensions installed in publicWidens the API surface PostgREST exposes.

What the output looks like

Illustrative — table names invented, shape is real:

severitycheckobjectfix
CRITICALSECURITY DEFINER open to PUBLICpublic.promote_user(uuid)revoke execute on function public.promote_user(uuid) from public;
CRITICALRLS disabledpublic.invoicesalter table public.invoices enable row level security;
HIGHRLS on, zero policiespublic.audit_logAdd a policy, or confirm it is service-role only.
HIGHPossible policy recursionpublic.team_members · members_readMove the lookup into a SECURITY DEFINER function.
PERFORMANCEauth.uid() re-evaluated per rowpublic.documents · owner_can_readWrap it: (select auth.uid()).

Nothing came back? Then these eleven failure modes are not present. That is a useful answer too.

Two of these deserve a note

#4 is the one that surprises people. In Postgres, a newly created function is executable by PUBLIC unless you say otherwise. On Supabase that means the anon key can call it. The REVOKE has to live in the same migration that creates the function, or there is a window where it is open.

#6 is not automatically a bug. A view without security_invoker runs with its owner's rights, which is exactly right when the view already filters by auth.uid() itself. Read the definition before you change it. Tools that flag every such view as an issue are generating noise.

Heuristics, stated plainly

Checks 9 and 10 are heuristics. #9 matches a policy expression that mentions its own table name — confirm by reading the policy. #10 matches only the leading index column, so an unusual composite index can produce a false positive. Both are deliberately tuned to over-report rather than miss something.

Tested against Postgres 17 / Supabase. Requires Postgres 15+ for the security_invoker check; everything else works on 12+.

Licence

MIT. Take it, fork it, put it in your CI.


Built by David Borrás, who fixes these for a living when reading the list is not what you wanted to do tonight.

Concepto505/supabase-audit

Eleven Supabase/Postgres production failure modes, found by one SQL file you paste into the SQL Editor. No install, no credentials shared.

0

0 commits

updated Sep 20, 2026

See the code
postgres
rls
row-level-security
security-audit
supabase

See what people are saying (1)

README

supabase-audit

One SQL file. Paste it into the Supabase SQL Editor, hit Run, get a list of the things that are about to bite you — worst first.

No install, no npm package, no connection string handed to a stranger, no account anywhere. It only SELECTs from system catalogs: it reads none of your table data, writes nothing, and sends nothing anywhere. It is one file so that you can read it before you run it.

curl -O https://raw.githubusercontent.com/Concepto505/supabase-audit/main/audit.sql

What it checks

#CheckWhy it matters
1Tables exposed to the API with RLS disabledThe anon key ships in your frontend bundle. Anyone can read every row.
2RLS on, zero policiesEvery client query returns an empty set. The classic "works in the SQL editor, returns nothing in the app".
3Policies open to anon with an always-true conditionA leak, unless that data is deliberately public.
4SECURITY DEFINER functions executable by PUBLICPostgres grants EXECUTE to PUBLIC by default. These are born open to the anon key.
5SECURITY DEFINER without a pinned search_pathPrivilege escalation: a caller can shadow a function it calls.
6Views that run as their ownerRLS on the underlying tables is not applied to the caller. Often correct — flagged for a decision, not as a bug.
7Secret-shaped columns on API-reachable tablesA leaked row hands over a live credential.
8auth.uid() re-evaluated per rowThe single biggest RLS slowdown. 20 ms vs 20 s on a large table.
9Policies that query their own tableinfinite recursion detected in policy for relation.
10Foreign keys with no indexEvery join and every cascading delete scans the whole table.
11Extensions installed in publicWidens the API surface PostgREST exposes.

What the output looks like

Illustrative — table names invented, shape is real:

severitycheckobjectfix
CRITICALSECURITY DEFINER open to PUBLICpublic.promote_user(uuid)revoke execute on function public.promote_user(uuid) from public;
CRITICALRLS disabledpublic.invoicesalter table public.invoices enable row level security;
HIGHRLS on, zero policiespublic.audit_logAdd a policy, or confirm it is service-role only.
HIGHPossible policy recursionpublic.team_members · members_readMove the lookup into a SECURITY DEFINER function.
PERFORMANCEauth.uid() re-evaluated per rowpublic.documents · owner_can_readWrap it: (select auth.uid()).

Nothing came back? Then these eleven failure modes are not present. That is a useful answer too.

Two of these deserve a note

#4 is the one that surprises people. In Postgres, a newly created function is executable by PUBLIC unless you say otherwise. On Supabase that means the anon key can call it. The REVOKE has to live in the same migration that creates the function, or there is a window where it is open.

#6 is not automatically a bug. A view without security_invoker runs with its owner's rights, which is exactly right when the view already filters by auth.uid() itself. Read the definition before you change it. Tools that flag every such view as an issue are generating noise.

Heuristics, stated plainly

Checks 9 and 10 are heuristics. #9 matches a policy expression that mentions its own table name — confirm by reading the policy. #10 matches only the leading index column, so an unusual composite index can produce a false positive. Both are deliberately tuned to over-report rather than miss something.

Tested against Postgres 17 / Supabase. Requires Postgres 15+ for the security_invoker check; everything else works on 12+.

Licence

MIT. Take it, fork it, put it in your CI.


Built by David Borrás, who fixes these for a living when reading the list is not what you wanted to do tonight.