If you’ve built anything with Bolt, Lovable, Cursor, or Replit Agent in the last two years, there’s a good chance your database is Supabase. We see it in the vast majority of the projects that land on our desk, and it’s usually not a bad choice on its own. The problem is what happens when an AI coding tool sets up Supabase AI code on your behalf and nobody goes back to check the rules it left behind. Supabase RLS mistakes are, without question, the single biggest data exposure risk we find in vibe-coded apps, and they’re almost always invisible until someone goes looking.
This article is about what those mistakes look like, why they happen so consistently, and how to check your own project without needing a computer science degree.
Why Supabase shows up in so many AI apps
Supabase is popular with AI coding tools for reasons that make total sense once you see them from the tool’s perspective. It’s a hosted Postgres database with an instant REST and realtime API layered on top, so an AI agent can spin up tables and start reading and writing to them within a single conversation, no server code required. It has built-in authentication, so “add a login” becomes a few generated files instead of a whole subsystem. And the free tier is generous enough that a prototype never hits a paywall while you’re still deciding if the idea works.
None of that is the problem. The problem is that Supabase’s power comes from exposing your database directly to the browser through a public API key. In a traditional backend, your server sits between the user and the database, and every request gets checked before any data moves. With Supabase, the browser often talks to the database almost directly, and the only thing standing between a stranger and your entire users table is a set of rules called row-level security. If those rules aren’t right, there’s nothing else in the way.
Row-level security in plain English
Row-level security, or RLS, is a feature built into Postgres itself, not something Supabase invented. Instead of controlling who can access a table as a whole, it controls who can access each individual row within that table. You write a policy that says, in effect, “a user can only see rows where the user_id column matches their own ID,” and Postgres enforces that on every single query, no matter where the query came from.
That’s a genuinely good model. It means the security check lives next to the data instead of scattered across a dozen frontend components that a developer, or an AI agent, might forget to update. But it only works if someone actually writes the policies, and writes them correctly. RLS is off by default on new tables, and a table with RLS off but a public API sitting in front of it is a table anyone on the internet can query if they know, or guess, the endpoint.
We’ve written before about how is your AI-built app secure covers exposure risks generally. Supabase is where that risk gets very concrete, very fast.
The “anyone can read everything” default
Here’s the pattern we see over and over. Someone asks an AI tool to “add user profiles” or “let people save their orders.” The tool creates a table, wires up the frontend to read and write to it through the Supabase client, and the app works immediately in the preview. Everyone’s happy. What often doesn’t happen is a proper RLS policy getting attached to that table, because the fastest way for the AI to get past an access error during development is to either leave RLS disabled or write a policy that just says USING (true), which translates to “allow this to everyone, always.”
That policy isn’t wrong exactly. It unblocks the immediate task. But it’s rarely revisited once the feature works, because from the outside a working app and a working-and-secure app look identical. Nobody sees the missing check in a screenshot. We’ve opened Supabase dashboards on client projects and found customer email addresses, order histories, even payment metadata sitting behind a table with RLS switched off entirely, reachable by anyone with the anon key, which is printed in the browser’s network tab on every page load. That key is meant to be public. It’s fine for it to be public. What’s not fine is a database that trusts it completely.
This is exactly the kind of thing that turns into a compliance problem, not just a technical one. If you’re handling any EU user data, our GDPR guide for founders is worth reading alongside this one, because an open RLS policy on a users table is precisely the sort of unauthorised access that data protection rules are written to prevent.
Migrations and schema drift
The second issue is quieter but just as costly. A lot of AI-assisted development happens through a chat interface making direct changes to the live database, rather than through versioned migration files that get reviewed and applied in order. That’s efficient for iterating on an idea. It’s a poor way to run a production system.
Over a few weeks of sessions, we regularly see schemas that have drifted a long way from anything documented anywhere. A column gets added to fix a bug, then renamed the next day, then a table gets dropped and recreated to “start fresh,” and the RLS policies that were carefully attached to the original table simply don’t exist on the replacement. Nobody removed them on purpose. They just didn’t come back, because Postgres doesn’t carry policies over when a table is recreated, and nothing in the AI tool’s workflow prompts a check afterwards.
This is a big part of why apps that worked fine in testing start misbehaving once real traffic hits them, a pattern we go into in detail in why your vibe-coded app breaks in production. Schema drift without migration history means there’s no reliable way to know what state your database is actually in, only what state you hope it’s in.
How to audit your own Supabase project
You don’t need to be technical to run a basic check yourself, and we’d genuinely encourage you to do this today rather than waiting for someone else to find the problem first.
Open your Supabase dashboard and go to Authentication, then Policies, or the Table Editor’s RLS view depending on which layout you’ve got. For every table that holds anything personal, financial, or private, confirm two things: that RLS is switched on, and that the policy attached to it actually restricts rows to the right user, rather than reading true or being blank. A table with RLS enabled but no policies at all is functionally the same as no security, because Postgres defaults to denying everything until a policy explicitly allows it, which sounds safe but often just means the app silently fails instead of failing securely.
Next, check your frontend code, or ask whoever built it, whether the service role key is used anywhere in client-side code. That key bypasses RLS entirely and should only ever exist on a server, never in a browser bundle. It’s a surprisingly common mistake, and one line of code is all it takes to hand out full database access to anyone who opens their browser’s developer tools.
Finally, if you can, try querying a table you don’t think you should have access to, using nothing but the public API and a fresh account. If it returns data, you’ve found your problem before someone else does.
We check database rules on every review, and Supabase RLS mistakes are one of the first things we look for because they’re so easy to miss and so costly when they’re missed. Get yours looked at free.