I built a Supabase RLS audit that's just SQL wrapped in a prompt A developer built a deterministic SQL-based audit for Supabase Row Level Security, wrapped in a prompt that instructs the model to run exact queries against Postgres catalogue views rather than investigate on its own. The tool, available at defencecore.com/audit without a signup wall, reports findings such as tables without RLS and policies with 'USING true' that expose data to anyone with the anon key. Every AI coding agent will happily "audit your RLS" if you ask it to. Mine did. It found three issues, declared the rest fine, and on the second run found two different issues and declared the rest fine. That's the problem. An agent asked to go look enumerates what it happens to notice, then phrases everything it never checked as an all-clear. The finding isn't deterministic, so the all-clear is worthless. So I inverted it: the prompt doesn't ask the model to investigate anything. It hands the model the exact SQL, tells it not to substitute its own, and leaves it doing the one part it's genuinely good at — explaining what a row in the result means. I put it up at defencecore.com/audit https://defencecore.com/audit with no signup wall. Every query reads a Postgres catalogue view — pg tables , pg policies , storage.buckets . Those describe the shape of your database: table names, policy definitions, bucket settings. There is no statement in the prompt that selects from an application table, and none that writes. That's a property of the SQL, not a promise you're asking a model to keep. You can read all of it before you paste it. You are auditing the Row Level Security configuration of my Supabase project. STEP 1 — Run this query exactly as written. Do not modify it, and do not substitute your own queries. It reads only Postgres catalogue views pg tables, pg policies, storage.buckets : it cannot read a single row of my application data, and it changes nothing. with findings as select 1 as rank, 'CRITICAL' as severity, 'Table has no Row Level Security - readable by anyone with the anon key' as finding, tablename as object from pg tables where schemaname = 'public' and not rowsecurity union all select 1, 'CRITICAL', 'Policy grants read access to everyone USING true ', tablename || ' - ' || policyname from pg policies where schemaname = 'public' and qual = 'true' union all select 2, 'HIGH', 'Policy accepts any new row WITH CHECK true - records can be reassigned', tablename || ' - ' || policyname from pg policies where schemaname = 'public' and with check = 'true' union all select 2, 'HIGH', 'Storage bucket is public - any object URL downloads without auth', name from storage.buckets where public union all select 3, 'REVIEW', 'RLS enabled but no policy - table returns nothing to my own app', t.tablename from pg tables t left join pg policies p on p.schemaname = t.schemaname and p.tablename = t.tablename where t.schemaname = 'public' and t.rowsecurity and p.policyname is null select severity, object, finding from findings order by rank, object; STEP 2 — Report the results as a table, most severe first, naming the exact table, policy or bucket. If the query returns no rows, say so plainly instead of looking for something else to report. STEP 3 — For each finding, state in one sentence what someone holding my public anon key could actually do with it. STEP 4 — Describe the fix for each finding in words. Do NOT run, apply, or offer to run any statement that modifies my database — no ALTER, no CREATE POLICY, no DROP, no migration. I will make the changes myself. STEP 5 — Finally, list which of these findings could silently come back after a future migration or a future prompt, and explain why a one-time audit cannot catch that. Paste it into Claude Code, Cursor, or anything else connected to your project. No agent handy? Copy the with findings as … block straight into the Supabase SQL editor — the SQL is the audit, the prompt is only the part that reads it back to you. Tables with no RLS. Readable by anyone holding your anon key, which ships in your frontend bundle by design. This is the one everybody knows about and still the one that shows up most. Policies where qual = 'true'. RLS is on, a policy is attached, and it WITH CHECK true . A user can reassign a record they own to somebody else. Public storage buckets. Correct for avatars. Wrong for the invoices and scanned documents that end up in the same bucket because public buckets are where uploads work on the first try. RLS on with no policy. Not a breach — but it's why a feature quietly returns an empty list instead of an error, and it's the usual prelude to somebody disabling RLS to "fix" it. The interesting step isn't 1 through 4. It's 5: which of these can come back silently? All of them. A migration re-creates a table without its policy. A prompt adds USING true to unblock a broken query. Someone flips a bucket public to debug an upload at 1am. Your audit output looks identical the next time you run it, because you don't run it again. That gap is why I ended up building Defencecore https://defencecore.com in the first place. A catalogue snapshot tells you a table is open; it can't tell you who already read it. That's in your Supabase logs, and on the lower plans those expire in days — Defencecore https://defencecore.com reads them as they arrive and keeps the log lines plus the incident built from them, so the evidence outlives the retention window that would have deleted it. Read-only, so it can explain a problem and never cause one. Run the prompt first though. It's free, it takes thirty seconds, and roughly everyone I've handed it to has found at least one REVIEW row they didn't expect.