Free workshop: build with AI without losing control of your project View the workshop →
vibebusters

← Full glossary

// glossary

Row Level Security (RLS): the rule that decides who sees which row

In short

Row Level Security (RLS) is a PostgreSQL feature that applies access rules to every row of a table, inside the database itself. On Supabase, it decides what a visitor or a signed-in user can read, create, update or delete.

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:

  1. List every table in the public schema and note whether RLS is enabled on each. Supabase shows a warning on exposed tables without RLS
  2. Read the policy names. A policy called "Enable read access for all users" on a profiles or orders table deserves an immediate question
  3. Look for true conditions. A using (true) policy opens reads to every targeted role. That is fine for a public catalogue, not for customer data
  4. Check the key used in the browser. It should start with sb_publishable_ (or be the legacy anon key). An sb_secret_ or service_role key in front-end code bypasses every policy
  5. 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_id condition on its own: Supabase recommends adding auth.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.

// faq

The questions we keep hearing

Is RLS turned on automatically in Supabase?

Treat it as an explicit step, table by table. The Supabase documentation says RLS must be enabled on every exposed table and suggests event triggers to enable it on new ones. The safe habit is to check the status of each table in the dashboard.

My Supabase key is visible in the browser: is that a vulnerability?

No, if it is the publishable key (or the legacy anon key). Supabase describes it as public by design and subjects it to RLS policies. The secret key (or the legacy service_role key) bypasses RLS and must never appear in browser code.

Does RLS replace access checks in the app code?

It is the last line of defence, and the most reliable one, because it applies however the database is queried. Checks in the interface still help the experience, but a hidden button protects no data: only the rule in the database does.

Check your app's access rules

The Scan reviews every table, every policy and every key, then documents the fixes with evidence for each finding.

See what the Scan covers