Supabase Row Level Security, explained for people who did not write the SQL
Row Level Security is a Postgres feature that decides, per row, who may read or change it. The rule lives in the database, so it applies to every request regardless of where that request came from. Supabase creates new tables with it disabled, which is why AI-built apps so often ship without it.
Part of the guide to shipping an app built with an AI coding tool.
The one-sentence version
Without Row Level Security, your database answers "can this person see this row?" with "yes". With it, the database checks a rule you wrote before returning anything.
That is the whole idea. The complexity is in writing rules that say what you actually meant.
Why it has to be in the database
You could check permissions in your app code instead, and plenty of apps try. The problem is that your app is not the only thing that can talk to your database. Anyone holding your public key can send a request directly, without ever loading your website.
Hiding a button changes what a person sees. It does not change what they can ask for. A rule inside the database applies whether the request came from your app, from a script, or from someone typing into a terminal, which is what makes it the only layer that actually holds.
Why it is off by default
Because turning it on with no policies blocks everything. A table with RLS enabled and no rules returns nothing to anyone, which would mean every new table broke your app until you wrote a policy.
So the default is permissive, and the burden is on you to notice. Nothing warns you at the moment it matters, and an app built without it works perfectly until someone looks.
What a policy actually looks like
A policy has three parts: which table it applies to, which operation it covers, and the condition that must be true.
| Part | Means | Typical value |
|---|---|---|
| Table | What it protects | Your table name |
| Operation | Which action it covers | SELECT, INSERT, UPDATE or DELETE |
| Condition | When it is allowed | The signed-in user owns this row |
The important part is that each operation needs its own thinking. A policy that lets people read their own rows says nothing about whether they can change them. Getting SELECT right and leaving UPDATE open is a common and expensive mistake.
The condition that looks right and is not
This is where most AI-built apps fail. The policy exists, so the dashboard shows the table as protected, but the condition only checks that somebody is signed in rather than that they own the row.
The effect is that every registered user can read every other registered user's data. It passes a glance. It fails the first person who tries.
A useful test of any policy: would it still let the wrong person through if that person signed up legitimately? "Is authenticated" almost always fails this test.
How to check yours without reading SQL
- Open the Supabase dashboard and go to Authentication, then Policies.
- For every table holding real data, confirm RLS is enabled. Anything not enabled is readable by anyone with your public key.
- Read each policy's condition. If it only mentions being authenticated, and not a user ID or ownership, it is not protecting users from each other.
- Confirm each table has policies for the operations it needs, not just SELECT.
- Then test it: sign up as a second user and try to reach the first user's data by changing an ID in the URL.
That last step is the one that counts. Everything before it is reading; that one is evidence.
What about storage?
Storage buckets have their own access rules, separate from your tables, and they are missed almost every time. If users upload anything private, check the bucket rules with the same scepticism you applied to the tables.
Want someone to look at yours?
Send us the repo or the live URL. You get a written list of what is broken, what each fix costs and what can wait — back within 24 hours, and yours to keep either way.
Get a $99 audit