RLS was on. It still let everyone read everyone's data.
We build with AI too — it writes code that runs, passes tests, and ships. So when we review AI-built Supabase apps, this is the pattern we run into more than any other, and it hides in plain sight.
Here's what it looks like in a migration:
create policy "Users can view their data"
on profiles for select
using (true);
Read that policy name, then read the using (true) under it. The name says "users can view their data." The code says "anyone can view everyone's data." Those are not the same thing — and the second one is what ships.
"RLS enabled" does not mean RLS is doing anything
If you have ever turned on Row Level Security in Supabase and felt safe, this is the part nobody warns you about. A policy with using (true) always passes. The lock is on the door, but the rule is "let everybody in."
It's worth being honest about how this happens, because it isn't carelessness. Someone asks the AI for a policy so users can read their own profiles. It returns something that runs, the app works in testing, every row comes back fine — because you're logged in as yourself. Nothing fails. Tests pass. The bug only exists when a different user shows up and reads rows that were never theirs, and that's exactly the case nobody tests by hand.
The AI wasn't wrong in a way you could see. It wrote code that worked. It just didn't write code that was safe, because "safe" means thinking about the attacker, and the AI was thinking about the happy path it was asked for. That's the gap. It's fast and genuinely good, but it doesn't sit there imagining the user who changes an id in the URL to see if your server stops them.
How to check your own policies in two minutes
Open the Supabase dashboard, go to the SQL editor, and run this:
select schemaname, tablename, policyname, qual
from pg_policies
where schemaname = 'public';
The qual column is the actual USING expression for each policy. Read it. If you see true sitting in there on a SELECT policy for a table that holds user data, that row is readable by anyone who can hit your API. Same story for with check on insert and update policies.
What you usually want instead is the policy tied to the logged-in user, something like:
create policy "Users can view their own profile"
on profiles for select
using (auth.uid() = user_id);
Now the rule says what the name always claimed: a user only sees rows where the user id matches theirs. Swap user_id for whatever your column is.
Three habits that catch this
None of them are clever, and all of them work:
- Read the
qual, not the policy name. The name is a comment. It can say anything. Thequalis the law. - Check every table that holds something personal. Not the obvious one alone — all of them. Most apps have a couple more than the owner remembers.
- Log in as a second test user and try to read the first user's rows. If you get data back, you found it before someone else did.
None of this is a reason to stop using AI. It builds fast and that's real. The catch is that the speed you get on building is speed you have to spend back on checking — because it will hand you something that runs and let you believe it's finished.
Want a second pair of eyes on the inside parts — the policies, the routes, the views? Read a full sample review or send us your repo.