How to check3 min readUpdated 2026-08-03

What is Row Level Security, and why your app needs it

When the browser talks to the database directly, the database is the only place left to enforce who reads what.


In a traditional app, your server sits between the visitor and the database and decides what they may see. In an app built with Lovable, v0, Bolt or a Supabase starter, that server is usually not there: the browser holds a key and queries the database directly. Row Level Security is what replaces the missing gatekeeper.

How it works

You attach a rule to a table. On every query, the database evaluates that rule against the person asking and silently filters out rows they may not see. Nothing is returned that the rule did not permit — no error, no partial data, just the rows they are entitled to.

The rule that covers most real cases

-- turn it on; until you do, policies are ignored entirely
alter table profiles enable row level security;

-- people may read only their own row
create policy "read own profile" on profiles
  for select using ((select auth.uid()) = user_id);

-- and write only their own
create policy "write own profile" on profiles
  for insert with check ((select auth.uid()) = user_id);

Enabled is not the same as effective

Two failures look identical from the dashboard. In the first, RLS was never enabled on the table, so any policies written are ignored completely. In the second, RLS is on and a policy exists — but it is written USING (true), which matches every row for every caller. Both report as configured. Both are open.

The only test that means anything

  1. 1Open your live app, press F12, Network tab, reload.
  2. 2Copy the apikey header from any request to *.supabase.co.
  3. 3Query a table holding user data with that key and nothing else — no login, no session.
  4. 4An empty array means the rule denied you. Rows mean it did not.

Query your own table as a stranger

curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*&limit=3"   -H "apikey: YOUR_ANON_KEY"   -H "Authorization: Bearer YOUR_ANON_KEY"

The mistakes that keep recurring

  • Writing policies without enabling RLS on the table first — the policies do nothing at all.
  • USING (true), added to unblock development and never revisited.
  • Covering SELECT only, leaving INSERT, UPDATE and DELETE unrestricted.
  • Trusting a role stored in user metadata the client can edit, which lets users promote themselves.
  • Bare auth.uid() instead of (select auth.uid()) — evaluated once per row instead of once per query, slow enough on a large table that people disable RLS to fix the timeout.
Paste this into your AI coding tool
Set up Row Level Security properly across my Supabase project.

1. List every table in the public schema and report whether RLS is
   enabled. Enable it everywhere it is missing.
2. For each table, tell me who should be able to read and write which rows,
   and wait for me to confirm before changing anything.
3. Write ownership policies covering all four commands — SELECT, INSERT,
   UPDATE, DELETE — using (select auth.uid()) = user_id, with WITH CHECK on
   inserts and both USING and WITH CHECK on updates.
4. For tables no browser should ever touch, enable RLS and write no policy,
   so they deny by default.
5. Never base a policy on a value the client controls, such as a role stored
   in editable user metadata.
6. Show me how to verify each table from outside with curl and the anon key.

Do NOT write any policy as USING (true), and do not disable RLS to make a
query pass.

Common questions

What is Row Level Security in simple terms?
A rule attached to a table that runs on every query and decides, row by row, whether the person asking may see or change that row. It lives in the database, so it applies no matter who is asking or which tool they use.
Why does an AI-built app need it more than a normal one?
Because tools like Lovable and v0 wire the browser straight to the database with no backend in between. There is no server code left to check permissions, so the database rules are the only enforcement point that exists.
Is checking that a policy exists enough?
No, and this distinction caused a real CVE. A policy written USING (true) matches every row for every caller — it exists, it passes an existence check, and it protects nothing. The only reliable test is querying the table as an anonymous user.
Do I need a policy on every table?
You need RLS enabled on every table. Tables no browser should touch are best left with RLS on and no policy at all, which denies everything by default — that is a feature, not an oversight.

This is one check out of 40+

Paste your site address and we run the whole list from the outside — leaked keys, open databases, unprotected pages — then hand you one prompt that fixes what we find. Free, about 30 seconds, no signup.

Check my site — free