Supabase rls audit
@shipcheck/supabase-rls-audit
Audit row level security in your Supabase migrations before you launch. Finds tables with RLS off, policies that let anyone write, policies that trust user_metadata, views that skip RLS, and risky SECURITY DEFINER functions, then drafts a fix migration for you to review. Reads .sql files only and never touches your database.
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
What it makes
Real output from this kit
- Sample file2 KB, download to openRls auditFrom the kit's tests
- Sample file1 KB, download to openRls auditFrom the kit's tests
- Sample file257 bytes, download to openRls auditFrom the kit's tests
How this works
- It is a skill: written instructions Claude reads when your request matches what the skill is for. You do not call it by name; just ask for the task.
- While it works, it reads and changes files in the places it lists, nowhere else.
- It runs these commands on your computer: node scripts/rls-audit.mjs.
- Nothing runs until you choose to use it, and you can remove it any time.
SKILL.md
Supabase RLS audit
In Supabase, your anon key ships to every browser. Row level security is the only thing between that key and your tables. This skill reads your migrations, replays them in order, and reports what the final schema lets strangers and other users do. It never connects to your database.
When to use
- Before launch, or before sharing the app with real users.
- After adding a table, a view, or a policy.
- "Is my Supabase locked down?", "can user A read user B's rows?", "why does the security advisor complain?"
- Not for: Storage bucket policies or Edge Function auth. Say so and offer a manual review instead.
Inputs
- The migrations folder, default
supabase/migrations. If the project has none (common with dashboard-only projects), ask the user to runsupabase db pullthemselves, or to paste the schema intosupabase/schema.sql, then point the script at a folder holding that file. - Which tables are meant to be public (blog posts, pricing, a public profile). Ask once, after the first run, only if there are
policy-public-readfindings.
Steps
Run the audit.
node scripts/rls-audit.mjs supabase/migrations --json out/rls-audit.json --md out/rls-audit.md- Read
out/rls-audit.mdand show the user the summary line and every high finding, in plain words: who can do what to which table. Example: "Anyone with your anon key can read and change every row ininvoices." - Check each finding against the code. Open the migration it names. Confirm the owner column (often
user_id,owner_id, oridon profile tables). Drop a finding only if you can say why it is safe, and say so in the report. - Ask about public tables. For each
policy-public-read, ask: "Is<table>meant to be readable by anyone on the internet?" Keep it if yes. - Draft a fix migration at
out/rls-fixes.sql(not insupabase/migrationsyet). One commented block per finding. Use(select auth.uid())rather than bareauth.uid(): same result, much faster on big tables. - Show the draft and ask before copying it into
supabase/migrations/<timestamp>_rls_fixes.sql. Tell the user to test it locally first, then apply it with their usual deploy. Re-run step 1 on the folder to confirm the high count is 0.
Fix patterns
-- Owner-only table
alter table public.notes enable row level security;
create policy "notes: owner reads" on public.notes for select to authenticated
using ((select auth.uid()) = user_id);
create policy "notes: owner writes" on public.notes for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy "notes: owner updates" on public.notes for update to authenticated
using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
-- View that should respect RLS
alter view public.note_counts set (security_invoker = true);
-- Admin check: app_metadata, which only the server can set
using ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')Output format
# RLS audit: <project>, <date>
High 3 | Medium 4 | Low 0 | Info 1
## Fix now
- invoices: RLS is off. Anyone can read and change every invoice. Fix in rls-fixes.sql block 1.
## Decide
- profiles is readable by anyone. You said this is intended: kept.
## Fine as is
- audit_log: RLS on, no policies, so only your server can reach it.Rules for findings
- High: RLS off on a table in an exposed schema; a write policy with
true; a write policy that only checks the user is signed in; any policy readinguser_metadata. - Medium: public read with
USING (true); open insert; views withoutsecurity_invoker; SECURITY DEFINER functions withoutset search_path. - Info: RLS on with no policies (server-only, usually fine).
Guardrails
- Never run SQL against a database, local or remote, and never run
supabase db pushordb reset. The user applies migrations. - Never edit an existing migration file. Fixes go in a new migration.
- Never put the service role key in client code as a "fix". If the app breaks after RLS is on, the fix is a policy, not a bigger key.
- The script parses common SQL. If a migration uses dynamic SQL or
DOblocks to create tables or policies, say the audit may miss them and review those by hand.
README.md
Supabase RLS audit
Checks the row level security in your Supabase migrations and tells you, in plain words, who can read or change each table. Then it drafts a fix migration for you to review, so you can launch without leaving your data open to anyone holding your public anon key.
For: anyone shipping a Supabase app, especially your first one. No security background needed.
Example
Input: supabase/migrations/ with a table that never got enable row level security and a policy like for all to authenticated using (auth.role() = 'authenticated').
Output (out/rls-audit.md):
5 tables, 4 with RLS on.
High: 3 | Medium: 4 | Low: 0 | Info: 1
## High
- public.invoices (rls-disabled): Row level security is off. Anyone with your
public anon key can read and change every row through the API.
- Fix: alter table public.invoices enable row level security;
- public.notes / Signed in users can do anything (policy-any-user-write):
any user can read and change other users' rows.Plus out/rls-fixes.sql, a draft migration with one commented fix per finding. Try it on the bundled example: node scripts/rls-audit.mjs examples/migrations.
What it checks
Tables with RLS off, USING (true) and WITH CHECK (true) policies, policies that only check "signed in", policies that trust user_metadata, views without security_invoker, materialized views, and SECURITY DEFINER functions without a fixed search_path.
What it can access
- Reads
supabase/migrations/**(andsupabase/schema.sqlif you paste one). - Writes the report to
out/, and a new fix migration tosupabase/migrations/only after you say yes. - Runs only its own script. No network, no database connection, no env vars.
Setup
None. Needs Node 22 or later.
Files
- README.md
- SKILL.md
- examples/migrations/20260101000000_init.sql
- examples/migrations/20260115000000_admin_and_views.sql
- kit.json
- scripts/rls-audit.mjs
Did it work?
0worked for them0did not work
Sign in to say whether it worked for you.
Nobody has said yet. Tried it? Tell the next person.
