Supabase · 9 min · Updated 26 Aug 2026
Supabase RLS checklist for before you launch
Row Level Security is what stops one customer reading another's rows. Ten checks to run before you launch.
Supabase gives you a Postgres database that your frontend talks to directly, using a public key that ships in your app. That design is fast, and it means one control decides whether a customer can read only their own rows or everyone's: Row Level Security, or RLS.
RLS is off by default on every new table. A table with RLS disabled, exposed through the API, is readable by anyone with your public anon key — which is in your JavaScript, so anyone at all. Turning RLS on with no policy does the opposite: it denies everyone. You need both the switch and a policy.
How to read your own exposure first
Before the checklist, see what the outside sees. Your anon key is public by design, so anyone can query your REST endpoint. A table without RLS answers that query in full.
GET /rest/v1/profiles?select=* HTTP/1.1
Host: <your-project>.supabase.co
apikey: <your public anon key, taken from your JS bundle>
HTTP/1.1 200 OK
[ { "id": 1, "email": "a-real-customer@example.com", ... } ]This is not a trick or a private key leak. The anon key is meant to be public. RLS is the thing that is supposed to stop the query returning other people's rows.
The 10-point RLS checklist
Run this against every table that holds user data before you launch. In the Supabase dashboard, tables are listed under Database, and policies under Authentication, then Policies.
- 01RLS is enabled on every table exposed through the API, not just the ones you remembered.
- 02Every table with RLS enabled has at least one policy. Enabled with no policy denies all access and breaks your app quietly.
- 03Read policies scope rows to the owner, typically
auth.uid() = user_id, nevertrue. - 04Write policies (
insert,update,delete) exist and are scoped separately. A read policy does not restrict writes. - 05Insert policies use a
WITH CHECKclause so a user cannot create a row owned by someone else. - 06No policy trusts a client-sent value like a
user_idin the request body to decide ownership. Useauth.uid(). - 07Views and functions that read protected tables do not bypass RLS. A
SECURITY DEFINERfunction runs as its owner and skips policies. - 08The service-role key is never in your frontend, your JavaScript bundle or a
NEXT_PUBLIC_variable. It bypasses RLS entirely. - 09Storage buckets holding user files are private, with policies on the
storage.objectstable, not public. - 10You tested the result with two real accounts: account A cannot read, edit or delete account B's rows through the API.
The mistakes coding agents make most
Agents write policies that look right and are not. Three patterns account for most of what turns up in a real audit.
A policy of USING (true). It reads as "allow" and means "allow everyone". Every authenticated user, and often every anonymous one, reads every row.
A read policy with no write policy. Reads are scoped correctly, so a quick test passes, but any user can still update or delete rows because the write path was never restricted.
Trusting the request body. A policy that checks a user_id sent by the client scopes nothing, because the client can send any value. Ownership has to come from auth.uid(), which the client cannot forge.
Testing RLS with one account cannot find these. You have to prove that account A tried to reach account B's specific row and was refused. That is exactly what a Launch Audit does with your test accounts, and why a passive check alone cannot return "Ready to ship" on authorization.
After you fix a policy, prove it
A fix to an RLS policy is easy to get subtly wrong, so verify it the same way you found the gap: sign in as account A, request account B's row, and confirm the API now refuses. Then check your own rows still load, because an over-tight policy that denies everyone is a different outage.
This detect-then-retest loop is the whole point: a finding is not fixed because you changed a line, it is fixed when the original request can no longer reach the data.