Supabase row-level security, explained for founders
6 min read · 19 Oct 2026
Supabase is a Postgres database with an API in front of it. That is the whole point and the whole risk: the API is reachable from the browser, with the same key every visitor gets, and the only thing deciding who can read which row is a set of rules called row-level security policies.
AI builders like Lovable, Bolt and Base44 use Supabase heavily because it lets an app talk to its database without a backend. When the builder creates a table, RLS is often enabled with no policies, or disabled, or given a policy that says "allow everything" so the demo works. The demo works. So does everyone else's access to your data.
What a policy says
A policy is one sentence attached to one table, for one action. In plain language:
- "A signed-in user may read rows of
projectswhereowner_idis their own user id." - "A signed-in user may insert a row into
projectsonly ifowner_idis set to their own id." - "Nobody may delete from
invoicesthrough the API."
Without the first sentence, every signed-in user can read every project. Without the second, a user can create rows that belong to someone else. Without the third, a curious visitor can empty the table.
The condition in each sentence uses auth.uid(), which is Supabase's way of saying "the id of whoever is making this request". A policy that does not mention auth.uid() or something equivalent is usually a policy that lets everyone in.
The three ways apps leak
RLS is off. The table was created and the switch was never flipped. Everything is readable with the public key.
RLS is on with no policies. Counter-intuitively this is safe: no policy means no access. The app then breaks, and the usual fix is the third case.
RLS is on with a permissive policy. A policy whose condition is true, added so the screen would load. This is the most common state of an AI-built app, because it is the one that makes the error go away.
There is a fourth leak that is not RLS at all: the service_role key. It bypasses every policy. It belongs on a server and nowhere else. If it is in your front-end code, nothing in this article matters until it is rotated.
The two-user test
You do not need to read SQL to know whether you are exposed. Create two test accounts. Sign in as the first and create some data. Then sign in as the second and, using the browser's developer tools, call the same API the app calls, asking for all rows of that table. If the first user's data comes back, the policy is wrong.
Do this for every table that holds something a customer would not want another customer to see: profiles, projects, messages, orders, files.
What good looks like
- RLS enabled on every table that the browser can reach.
- One policy per action per table, each with a condition that mentions the current user.
service_roleused only from server code, with the key kept in a secret manager.- The two-user test written down and repeated after every schema change.
Who should do this
Writing policies is an hour's work for someone who has done it before and a weekend of confusion for someone who has not. If you are about to put real users on an app built this way, have the policies read before launch, not after the first support email that says "I can see someone else's invoices". The Launch Readiness Check covers exactly this, with the verdict in writing.