Row Level Security (RLS) works like an automatic filter added to every query: before returning a row, the database checks that the person asking is allowed to see it. In an app built with Lovable, Bolt or another tool that relies on Supabase, it is often the only thing separating one customer's data from another's.
Why RLS matters so much in an AI-built app
In a classic app, the browser talks to a server, and the server talks to the database. The server does the filtering. With Supabase, the browser queries the database directly, with a public key that any visitor can see. That is by design: the Supabase documentation describes the publishable key as "safe to expose" in a web page or a mobile app.
The consequence is simple. Protection does not come from the key, it comes from the rules written on each table. A table with good policies stays closed even if the whole planet knows the key. A table without a proper policy opens up to anyone who can send a request.
AI tools quickly produce tables and screens that work. They write policies too, but their implicit goal is for the feature to work on the first try. An overly broad policy passes every visual test: the screen loads, the data shows up, and nobody notices that user A could also read user B's data. In May 2025, an analysis of 1,645 apps published with Lovable found 170 projects whose personal data was reachable this way. Lovable now runs a quick scan on every publish that flags, among other things, tables without per-record access control. It is a very good safety net, and it does not replace checking that each policy says what you think it says.
How RLS works, without the jargon
An RLS rule (a "policy") answers three questions: for which operation (read, create,
update, delete), for which role (anonymous visitor or signed-in user), and under what
condition. For example: "a signed-in user can read the rows of the invoices table whose
user_id column matches their own ID".
Two behaviours often surprise people:
- RLS enabled with no policy: the table is closed. Supabase states that no data is accessible with the publishable key until policies exist. The app shows empty lists, which sometimes leads to a rushed "everyone can read everything" policy
- RLS disabled: the table only follows the role's general privileges, which often means it is open to anyone holding the public key
For changes, Supabase distinguishes two clauses. USING decides which existing rows can
be touched. WITH CHECK decides what the row must look like after the operation. Without
WITH CHECK, a user could edit their own row and hand it over to someone else.
How to check your app's RLS
You can run a first check without writing code, from the Supabase dashboard:
- List every table in the
publicschema and note whether RLS is enabled on each. Supabase shows a warning on exposed tables without RLS - Read the policy names. A policy called "Enable read access for all users" on a profiles or orders table deserves an immediate question
- Look for
trueconditions. Ausing (true)policy opens reads to every targeted role. That is fine for a public catalogue, not for customer data - Check the key used in the browser. It should start with
sb_publishable_(or be the legacyanonkey). Ansb_secret_orservice_rolekey in front-end code bypasses every policy - Test with two accounts. Sign in as account A, note the ID of a record, then try to read it as account B. It is the most telling test
Our article on Supabase access rules walks through this test step by step, and the checklist places it among the other checks to run before real users arrive.
A concrete example
A booking platform built in a few weeks stores appointments in a bookings table. The
generated policy says: "signed-in users can read bookings". It works perfectly in the
demo, with a single account. The day a hundred customers sign up, each of them can read
the other ninety-nine's calendars, with names and phone numbers.
The fix takes a few lines: the policy becomes "signed-in users can read bookings where they are the customer or the provider", with an index on the columns involved. The real work is finding every table in the same situation, then checking that the app still works once the rules are tightened.
The most common mistakes
- The "open everything" policy added to unblock a screen, then forgotten
- The table added late (logs, notifications, files) that never got a policy
- The
auth.uid() = user_idcondition on its own: Supabase recommends addingauth.uid() IS NOT NULL, otherwise the policy fails silently for signed-out visitors and can behave unexpectedly - The secret key used in the browser to push through a feature that kept hitting a policy
- Columns without an index: a policy that filters on an unindexed column forces the database to scan the whole table on every query. The app slows down as data grows
The takeaway
RLS is a sound mechanism, and it protects an app very well once its policies are right. The weak spot is rarely the tool: it lies in rules written quickly, iteration after iteration. The vibebusters Scan reviews every table, tests cross-account access and delivers the list of policies to fix, ranked by priority.