Row level security, or RLS, is a setting on your database that says “user A can only see rows that belong to user A.” Without it, every user of your app can read every other user's data. Their messages. Their bookings. Their payment history. The query runs, the data comes back, the AI tools don't warn you, and you don't find out until someone shows you what they can see.
This is the most common security issue I find in vibe-coded apps. Not the most exotic, not the cleverest. Just the most common. It shows up in roughly four out of every five Lovable apps and three out of every four Bolt apps that come through our audit.
Here's what it is, why it keeps happening, and how to fix it before someone with curl finds it first.
01What RLS actually is, in plain English
When your app stores data, it lives in tables. A messages table. A bookings table. A users table. When the app needs data, it asks the database: give me the rows from messages.
Without RLS, the database hands over all the rows. Every message, from every user, regardless of who's asking. The only thing standing between user A and user B's messages is your front-end code, which is running in user A's browser and can be turned off with browser developer tools in about 30 seconds.
RLS is a layer at the database itself that says: when this table is queried, only return rows where the row's user_id matches the ID of the user making the request. The check happens before the data ever leaves the database. It cannot be bypassed by editing the front-end. It cannot be bypassed by a malicious request crafted in a terminal. It is the only authentication boundary that actually counts.
02Why your AI didn't add it
Lovable, Bolt, Cursor and the rest are optimized to make things work in the demo. RLS does not affect the demo. The screens look identical whether RLS is on or off. The only times you notice RLS is missing are:
When an auditor checks. When a second user signs up and you realize they can see the first user's data. When someone curious enough to open developer tools finds out.
None of these things happen during the build. They happen later, in production, with real users. The AI is solving for the moment of “ship it”; RLS is a problem of “what happens after ship.”
This is also why your AI tool will rarely surface it unless you specifically ask. “Add row level security policies to all my Supabase tables and write the SQL” is the prompt that works. Without it, you get a working app with the RLS toggle off and no policies defined.
03How to tell if your app has RLS (in 2 minutes)
If you're using Supabase, which most Lovable and Bolt apps are:
Step 1. Open your Supabase dashboard.
Step 2. Go to the table editor.
Step 3. Click on each table. Look at the top of the panel. You'll see either “RLS enabled” in green, or “RLS disabled” in red.
If you see “RLS disabled” on any table that holds user data, that's the problem. Every user can read everything in that table, full stop.
04How to add RLS to a Supabase app
For each table that stores user-specific data (messages, bookings, posts, anything where rows belong to specific users), you need two things: a user_id column (or equivalent) that stores who owns each row, and an RLS policy that uses that column.
In the Supabase SQL editor, the basic policy for a messages table looks like this:
Repeat for update and delete. Repeat for every table. For an app with five to ten tables, this is 20 to 30 minutes of work.
If you're not comfortable in the SQL editor, the Supabase dashboard has a policy template flow. It will not write perfect policies, but it gets the basic shape right and you can refine from there.
05The four mistakes I see most often
Enabling RLS without writing policies. This silently breaks half your app, you panic, you turn RLS back off, you forget to ever come back. Fix: enable RLS and write the basic policy in the same session, before you close the tab.
Using auth.uid() is not null as the policy. This means “anyone logged in can see this row,” barely better than no RLS. Any user with an account can read every other user's data. Fix: the policy must compare the user's ID to the row's owner ID, not just check that they're logged in.
Forgetting with check on inserts and updates. Without it, users can create rows that claim to belong to other users. Fix: every insert and update policy needs both a using clause (what they can read) and a with check clause (what they can write).
Trusting the front-end to set user_id. Don't have your app set user_id in the request body. Set it from the authenticated session on the server, or have the database default it from auth.uid(). Otherwise users can simply tell your app they're someone else.
06Beyond Supabase
If you're not on Supabase, the principle is the same: the access check belongs at the data layer, not the application layer.
Postgres directly. Same syntax as the example above. RLS is a Postgres feature; Supabase just exposes it nicely.
Firebase. Security rules in firestore.rules. Same idea, different syntax. The default ruleset on a fresh Firebase project allows everything; the equivalent of RLS being off.
Convex. Query functions enforce access in code on the server. Less declarative than RLS, but still happens at the data layer where it should.
Whatever your stack, the question to ask is: if a hostile user pointed curl at my API endpoint and changed the user ID in the request, would my database hand them someone else's data? If the answer is yes or “I don't know,” you have an RLS-shaped problem.
→What to do this week
If you have a vibe-coded app with real users, open Supabase and check the RLS column on every table. Twenty minutes. If it's red anywhere users have data, fix that before anything else this week.
If you'd rather we check, run the free three-minute audit. It tells you exactly which tables are exposed and what policies are missing. No account, no sales call.
If you want context on the bigger picture (what vibe coding is, why these gaps exist, which tools introduce which risks), start with the 2026 vibe coding guide.