Skip to main content

Supabase row level security checklist for AI-built apps

Last updated

Enable row level security on every table in an exposed schema, write policies that compare rows to auth.uid(), add both USING and WITH CHECK clauses for writes, make views security_invoker, review SECURITY DEFINER functions, lock down storage buckets, keep the service_role key on the server, and test everything with only the anon key.

Why RLS is the whole security model

Supabase exposes your tables through an HTTP API, and every front end ships the anon (or publishable) key so the browser can call it. That key is public by design. Row level security policies decide which rows each request may read or change, so a table without RLS, or with a policy that returns true for everyone, is readable by anyone who copies the key from your site.

AI coding tools often “fix” a permission error by loosening a policy until the error goes away. The app works again, and every user can now see every other user’s data. Re-check policies after any change an AI agent makes to the database.

The checklist

  1. Enable RLS on every table in the public schema (and any other schema exposed through the API). Supabase’s Security Advisor flags tables where it is off.
  2. Give every table at least one policy; RLS with no policies blocks everything, which is safe but usually not what you want.
  3. Scope read policies to the signed-in user, for example user_id = auth.uid(), or to a membership table for team data. Never use a policy that is simply true for sensitive tables.
  4. For INSERT and UPDATE, add a WITH CHECK clause so users cannot write rows that belong to someone else.
  5. Create views with security_invoker = true (Postgres 15 and later), otherwise a view runs with its owner’s rights and skips RLS on the tables behind it.
  6. Review every SECURITY DEFINER function: it runs with the creator’s rights, so it must check auth.uid() itself.
  7. Make storage buckets private unless every file in them is meant to be public, and add policies on storage.objects for uploads and downloads.
  8. Keep the service_role or secret key only in server code and Edge Function secrets. It bypasses RLS completely.
  9. Test with only the anon key and no session: request each table through the REST API and confirm you get no rows that belong to other users.
  10. Repeat the test after every schema change, and add it to your deploy checklist or CI.

Common mistakes and the safe pattern

MistakeWhat happensSafe pattern
RLS disabled on a tableAnyone with the anon key reads and writes every rowalter table … enable row level security, then add policies
Policy using (true)Same as no RLS for that operationusing (auth.uid() = user_id)
UPDATE policy without WITH CHECKUsers can move rows to another user_idwith check (auth.uid() = user_id)
View over a protected tableView returns all rowscreate view … with (security_invoker = true)
service_role key in the front endFull database access for every visitorServer-only secret; rotate it if it ever shipped
Public storage bucket for uploadsFiles readable by URLPrivate bucket and signed URLs

Questions

Can Defexity test my RLS policies?
From the live URL, Defexity finds service_role keys and database credentials in your public JavaScript. With GitHub connected it flags migrations that create tables without RLS or grant writes to the anon role. Proving a policy leaks data needs requests with your anon key, which should only be done on a project you own.
Is the anon key safe to expose?
Yes, when every table has correct RLS. It identifies your project; it does not grant access by itself.
Does the publishable key change anything?
Supabase’s newer publishable and secret keys replace anon and service_role. The rule is the same: publishable keys ship to browsers and rely on RLS, secret keys stay on the server.

Related Defexity checks

Sources

Defexity reports potential risks it can see from outside your app. It is not a guarantee of security and not legal advice.