Last time I gave you a 10-minute checklist, and #1 on it was "is RLS actually on?"
A bunch of you replied with some version of: okay… but what IS that, and how do I do it right? Fair. RLS is the single most important security setting in a Supabase app — and it's also the one AI gets wrong the most, in a way you will never catch just by looking at your app.
So here's RLS, explained the way I wish someone had explained it to me. No prior knowledge needed.
First, the one-sentence version
RLS (Row Level Security) is a rule that decides, for each row in your database, who is allowed to see or change it.
That's it. "Row" just means one entry — one user, one order, one message. RLS is the bouncer standing at each row, checking IDs.
The analogy that made it click for me
Think of a database table as a filing cabinet full of drawers. Each drawer is one person's data.
Your app talks to that cabinet using a key called the anon key — and here's the scary part: that key is public. It's shipped to every visitor's browser. It has to be, that's how the app works.
So the only thing standing between "a normal app" and "anyone on the internet can open every drawer" is RLS.
RLS off = the cabinet has no locks. The public key opens every drawer. Everyone's data, exposed.
RLS on, with the right rules = the cabinet checks who you are and only opens your drawer.
When AI builds your app, the cabinet ships unlocked by default. The app works perfectly in the demo, because in a demo there's only you. The problem only exists once real people — and real strangers — show up.
The three states everyone gets stuck in
Here's where it gets tricky, because there are three situations, and two of them feel like dead ends.
State 1 — RLS OFF. Everything works, everything is exposed. This is where most AI-built apps start. Dangerous, but at least it's obvious once you know to look (your Supabase Security Advisor screams about it).
State 2 — RLS ON, but no rules yet. You flip RLS on… and suddenly your app shows nothing. Empty lists, blank screens. This freaks people out, and it's the #1 reason they panic and turn RLS back off — straight back to exposed.
Here's what's actually happening: Postgres (the database under Supabase) defaults to "deny everything" the moment RLS is on. No rule = no access. So "my app broke when I turned on security" doesn't mean security is bad. It means you've locked the cabinet and haven't told it who's allowed in yet.
State 3 — RLS ON, with a rule that lets everyone in. This is the dangerous one, because it looks solved. More on this next — it's the trap that gets almost everyone.
The trap: a rule that looks perfect and protects nothing
When your app breaks in State 2, the fastest way to "fix" it is to add a rule that says allow everything. In Supabase that looks like a policy using the expression USING (true).
true means "yes, always, for everyone." So USING (true) re-opens every drawer — you're right back to exposed, except now it looks locked. RLS is on! There's a policy! Everything works again!
AI loves this fix, because its job was "make the error go away," and USING (true) makes the error go away. Your Security Advisor will flag these as "RLS Policy Always True." If you see that, treat it like RLS was never on.
The subtler trap: "are you logged in?" vs "is this yours?"
This is the one I want you to really get, because it's where even careful people leak data — and where AI is most confidently wrong.
A good rule has to answer one question: should THIS person see THIS row? There are two very different ways to write that, and they look almost identical:
auth.role() = 'authenticated'means "are you logged in?" If that's your rule, then every logged-in user can read every other user's rows. Log in, see everyone's data. It passes every "does it work?" test. It is wide open to your entire user base.auth.uid() = user_idmeans "is your ID the owner of this row?" This is almost always what you actually wanted. Only the owner sees their own data.
auth.uid() is "your personal ID." auth.role() is just "logged in or not." The AI reaches for auth.role() constantly, because it makes the app work — and a working app looks like a secure one. It isn't.
No tool, no scan, and no AI prompt can know which one you meant. That requires understanding your app: who owns what, who's allowed to peek, who's an admin. That judgment is yours. This is the part I keep telling people is irreducibly human.
So how do you actually get it right?
You don't need to be a developer. You need to answer one question per table, in plain English, before you write any rule:
"For this table, who should be able to see a row, and who should be able to change it?"
A few common answers and what they translate to:
"Only the person who created it." → owner-only. The rule checks
auth.uid() = user_id. (Your table needs a column storing the owner's ID — usuallyuser_id.)"Anyone logged in can read it, but only the owner can edit it." → that's actually two rules: a read rule that's open to logged-in users, and an edit rule that's owner-only.
"Only admins." → a rule that checks the user's role against your admins.
The key mental shift: you write a rule per action — one for reading, one for creating, one for editing, one for deleting — and for each you decide who. Tedious, yes. But that "who" is the whole game, and it's the part only you can answer.
When you're ready, the cleanest way to do it: open Supabase's SQL or policy editor and describe, in plain language, exactly who should access what — "only the user whose id matches user_id can select or update their own row" — and have the AI write the policy to match. Then verify it (next section). The AI is great at writing the SQL once you've supplied the intent. It's terrible at guessing the intent for you.
Check yours in 5 minutes
Supabase → Database → Advisors → Security. It flags tables with RLS off and policies that are "Always True." Start there.
Test it like an attacker. Make a second test account. Log in as user B and try to load user A's data. If you can see it, your rule is checking "logged in?" when it should be checking "is this yours?"
Watch the empty screens. If a feature suddenly returns nothing after you added security, that's State 2 — you locked the table and haven't written the read rule yet. Don't turn RLS off. Write the rule.
Why this is never "done"
Here's the part that actually broke me on my first real app: getting RLS right once isn't enough. Every time you re-prompt the AI to add or change a feature, it can quietly rewrite or re-break a policy you'd already gotten right. I fixed one rule, another broke; fixed that, the first came back. I eventually deleted two months of work and rebuilt from scratch.
A policy isn't a thing you set and forget. It's a thing that drifts every time the app changes. That's exactly why I'm building SnapDeck — it watches for this stuff continuously, in plain English, on your own machine, with your keys never leaving your computer. (Never paste your database keys into a random website to "check" them. Ever.)
It's in early access. If you want in — or you just want the next note, the real numbers, what I ship and what breaks — subscribe below. Solo dev in Jeju, $0 → $1K MRR in 90 days, in public.
Go run those two test accounts against your app. It's the fastest way to find out if "are you logged in?" is quietly the only thing protecting your users.
— Json
P.S. If your Security Advisor shows "RLS Policy Always True" anywhere, reply and tell me which table. I'll point you at the right fix.