Row-level security: the wall your AI-built app is missing
Of everything I check when I go through an AI-built app, one gap scares me more than the rest combined: Supabase row level security, or the lack of it. Not because it's exotic. Because it's invisible right up until the moment it isn't, and by then it's someone else's data on someone else's screen.
Here's the distinction that matters, and it's smaller than people expect. "It works" means you logged in, saw your stuff, and everything looked right. "Each user only sees their own data" means the database itself refuses to hand back anyone else's rows, no matter who asks or how. Those are two completely different claims about your app, and an AI coding tool will happily ship you the first one while leaving the second one undone. The demo passes either way. Only one of them is actually safe.
So what is row-level security, in plain terms? Most apps store data in tables — one row per user, per order, per message, whatever the object is. Without row-level security, a database table is like a filing cabinet with no folders: every drawer is unlocked, and the app is just trusting that whoever's asking for a file only asks for their own. Row-level security is the policy that gets attached directly to the table, at the database layer, saying: a request can only see rows where this row's owner matches the person making the request — enforced every time, regardless of what the app code above it does or forgets to do.
That "regardless of what the app code does" part is the whole point, and it's exactly why AI tools skip it. When Cursor, Claude, Lovable, or Bolt scaffold your backend, they're solving the problem you handed them: show the logged-in user their dashboard, their orders, their messages. The straightforward way to do that is to query the table and filter by the current user in the app code — and that works perfectly, every time, in every demo you'll ever click through yourself, because you're always asking as you. Row-level security is a second, independent lock that most tools don't turn on unless you specifically ask for it, because turning it on requires knowing it's a category of protection that exists in the first place. If nobody tells the AI to enable it, it typically isn't enabled — the table just answers whatever it's asked.
I wrote about this same failure pattern more broadly in what your AI-built app is missing before launch — the tool builds exactly what you describe, and this is the clearest single example. Nobody describes "and also make sure a stranger can't just ask for someone else's row directly," so nobody gets it.
The one-question test you can run yourself right now
You don't need to read a line of code to get a real signal here. Log into your own app with one test account. Open a screen that shows something private — an order, a profile, a message thread, a dashboard. Open your browser's dev tools, find the network request that screen is making, and look at the ID it's sending — an order ID, a user ID, whatever's in the URL or the request body. Now just change that ID to a number or value that isn't yours, and reload.
If you get someone else's data back, or an error message that reveals it exists, the wall isn't there. If you get a clean "not found" or "not authorized" no matter what ID you throw at it, that's a real, database-enforced boundary — not just a frontend that politely didn't show you a link to it.
Most non-technical founders have never run this test on their own app, and there's no shame in that — it's not the skill that got the app built, it's an adversarial skill, and it's a different one. But it takes two minutes, and what it tells you is about as close to a launch/no-launch signal as exists for an AI-built app.
Why this one is non-negotiable
Every layer I check has some room for judgment — you can launch with a mediocre mobile layout and fix it in week two, you can launch without a tested restore process if you're disciplined about backups otherwise. Data isolation doesn't get that same leeway. If a stranger can reach another user's data by changing a number in a request, that's not a rough edge to polish later — it's a hard stop, full stop, regardless of how good the rest of the app looks. It's one of exactly two non-negotiables in the full 13-layer check we run in the Launch-Ready Audit, alongside missing auth on protected actions.
The good news is that fixing it, once you know it's missing, is usually a matter of days, not a rebuild — turning on the database's built-in policies and writing the rules for who owns what. The hard part was never the fix. The hard part is that almost nobody checks before a stranger does it for them.
Your Turn: run the ID-swap test above on one screen in your app today. If it holds, good — you've confirmed the single scariest gap isn't one of yours. If it doesn't, that's worth fixing this week, not after launch.
See how the Launch-Ready Audit works →
— Kyle Tysvaer, Founder, Insightful Eye Marketing