← Leakproof

The Supabase RLS checklist for AI-built apps

Lovable, Bolt, Cursor and v0 make Supabase apps fast. They also make it easy to ship a database anyone can read. Your frontend holds the anon key, and that's by design: anyone can see it. The only thing standing between that key and your users' data is Row Level Security. Here's how to check it in about ten minutes.

1. Find tables with RLS off

Run this in the Supabase SQL editor:

select schemaname, tablename
from pg_tables
where schemaname = 'public' and rowsecurity = false;

Every row returned is a table that the anon key can read and write through the REST API, whatever your UI shows. Fix:

alter table public.<table> enable row level security;

With RLS on and no policies, nobody can access the table (except your server using the service_role key). Now add only the access you need.

2. Find policies that let everyone in

select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
  and (qual = 'true' or with_check = 'true');

using (true) on a select policy means "anyone can read every row", which is fine for a product catalog and a leak for anything per-user. On insert/update/delete/all it means anyone can change anyone's data. The usual fix is an ownership check:

drop policy "<name>" on public.todos;
create policy "owners only" on public.todos for all to authenticated
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

3. Prove it from the outside

Policies are easy to misread. Test with the same key an attacker has (your anon key, from your site's JS):

curl "https://<project>.supabase.co/rest/v1/<table>?select=*&limit=5" \
  -H "apikey: <anon key>"

Signed-out, this should return [] or an error for any private table. Then sign in as test user A and confirm you can't see user B's rows.

4. Make sure the service_role key never reaches the browser

The service_role key ignores RLS entirely. It must only live in server code (API routes, server actions, edge functions). Search your code for SERVICE_ROLE and check none of it is in a 'use client' file, a NEXT_PUBLIC_ / VITE_ variable, or committed to git. If it ever was, rotate it.

5. Watch for the service_role shortcut in server actions

A common AI-generated pattern: a server action creates a service_role client "to make it work", then updates a row by id. RLS no longer applies, so any signed-in user can change any row by sending a different id. Use the user's own Supabase client in server code (so RLS applies), or add .eq('user_id', user.id) to every query.

6. Check storage buckets

select id, public from storage.buckets;

A public bucket serves any file to anyone with its URL, with no policy check. Fine for avatars; not for invoices, uploads or exports. Make those private and use createSignedUrl().

7. Check views and functions

Views run with their creator's permissions by default, so a view over a protected table can leak it. On Postgres 15+ use create view ... with (security_invoker = true). Functions marked security definer also bypass RLS; make sure they check auth.uid() themselves.

Check your repo automatically. Leakproof's free scanner reads your migrations and code and flags tables without RLS, allow-all policies, service_role keys in client code, public buckets and more. It runs locally:
npx github:beingRealFrank/leakproof
Want someone to go through all of this, plus your live project, and send fix PRs? See the fixed-price audits.

More checklists: Firebase rules · Secret keys in your frontend