Skip to content
SlashClone
Made by@shipcheck

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.

SkillDevOpsv1.0.0 (latest)Tested
Activity
New: be the first to try it

Real output from this kit

How this works

  1. 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.
  2. While it works, it reads and changes files in the places it lists, nowhere else.
  3. It runs these commands on your computer: node scripts/rls-audit.mjs.
  4. 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 run supabase db pull themselves, or to paste the schema into supabase/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-read findings.

Steps

  1. Run the audit.

    node scripts/rls-audit.mjs supabase/migrations --json out/rls-audit.json --md out/rls-audit.md
  2. Read out/rls-audit.md and 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 in invoices."
  3. Check each finding against the code. Open the migration it names. Confirm the owner column (often user_id, owner_id, or id on profile tables). Drop a finding only if you can say why it is safe, and say so in the report.
  4. 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.
  5. Draft a fix migration at out/rls-fixes.sql (not in supabase/migrations yet). One commented block per finding. Use (select auth.uid()) rather than bare auth.uid(): same result, much faster on big tables.
  6. 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 reading user_metadata.
  • Medium: public read with USING (true); open insert; views without security_invoker; SECURITY DEFINER functions without set 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 push or db 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 DO blocks 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/** (and supabase/schema.sql if you paste one).
  • Writes the report to out/, and a new fix migration to supabase/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.