Supabase RLS Explained: It's a WHERE Clause on Every Query

Supabase RLS in plain English: it's a WHERE clause your database adds to every query. Learn how Row Level Security works, why AI tools skip it, and how to check yours.

Barret7 min read

If you built your app with Lovable, Bolt, v0, Cursor, or Replit, there's a good chance your database is Supabase — and a good chance you've seen a warning about something called RLS. Maybe a scanner flagged it. Maybe a Reddit thread scared you. Either way, you're not sure what it means or whether your users' data is sitting in the open.

Here's the reassuring part: Row Level Security is one of the most learnable ideas in app security. If you understand a spreadsheet filter, you already understand 90% of it.

This post explains exactly what RLS is, why it matters more for AI-built apps than almost anything else, and how to tell in a few minutes whether yours is doing its job.

⚡ TL;DR

  • RLS (Row Level Security) is a rule your database attaches to every single query — like an automatic WHERE clause that decides which rows each user is allowed to see or change.
  • Your Supabase anon key is public by design and is not the problem. The problem is tables that have RLS turned off, or a policy that says "allow everyone." That combination is what leaks data.
  • You can check it yourself in minutes — and if it's wrong, the fix is usually a few lines of SQL.

What RLS actually is

Row Level Security is a Postgres feature (Supabase is Postgres under the hood) that filters table rows per user, on every query, inside the database itself.

Think of a support_tickets table. Without RLS, a query like select * from support_tickets returns every ticket from every customer. With RLS, the database silently rewrites that same query so it behaves like:

select * from support_tickets
where user_id = auth.uid();   -- the currently logged-in user

You didn't write that WHERE clause. The database added it for you, automatically, because a policy told it to. That's the whole idea: RLS is a WHERE clause on every query that you can't forget to include, because the database enforces it for you.

This matters because Supabase apps talk to the database directly from the browser. There's often no backend server sitting in the middle to check "is this person allowed to see this?" The database itself is the bouncer — and RLS is the list it checks. Turn that list off, and the bouncer waves everyone through.

Why this is the #1 issue in AI-built apps

When you ask an AI builder to "add a database and let users save their profiles," it optimizes for one thing: making the feature work in the preview. A table with RLS switched off works perfectly in a demo — you can read and write rows, the app looks done. The security hole is invisible until someone goes looking.

And people do go looking. The first published CVE for this exact category, CVE-2025-48757, was disclosed in May 2025: a scan of 1,645 Lovable-built apps found 170 of them (about 10%) leaking real user data across 303 endpoints — all through tables with missing or permissive RLS. The same pattern has since shown up in apps built on Bolt, v0, Cursor, and Replit. It is not a Lovable bug; it's a default that the whole vibe-coding ecosystem inherits.

The part most scanners get wrong

Here's the thing that trips up worried founders — and even some security tools.

When data leaks from a Supabase app, it leaks through the anon key, the public API key your app ships in the browser. So people understandably panic: "My API key is exposed! I need to rotate it!"

You don't. The anon key is public by design. It's meant to live in your frontend. Supabase built the entire permission model around the assumption that this key is visible to everyone. Rotating it changes nothing about your security.

🐺 Not a real problem

An exposed Supabase anon key is not a leak. It's supposed to be public. If a tool told you to rotate it, that tool is crying wolf. The real question is always: do your tables have RLS enabled, with a policy that actually restricts rows?

The key is just the front door handle — anyone can grab it. RLS is the lock. A leak happens when the lock is missing, not when the handle is visible. (The one key you must never ship in the browser is the service_role key, which bypasses RLS entirely. We cover that in service_role vs anon key.)

How to check your own app

You can sanity-check your Supabase project in about five minutes.

  1. Open the Supabase dashboard → Authentication → Policies (or Database → Tables). Every table that holds user data should show RLS enabled. If you see "RLS disabled" on a table with real data, that's your leak.
  2. Look at the policy itself, not just the on/off switch. A table can have RLS enabled and still leak if its policy is USING (true) — that means "this rule matches every row," which is the same as no protection. (More on that trap here: the USING(true) trap.)
  3. Test as a real anonymous user. The most honest check is to query your public API with just the anon key and see what comes back. If you can pull other people's rows, so can anyone else.

Not sure if your tables are actually protected?

Run a free, read-only scan of your live app. Is My Site Hackable? checks your real endpoints the way an attacker would — and tells you which tables are exposed, without the anon-key false alarms.

Scan my app free →

The fix

If you find a table without RLS, the fix is usually short. Enable RLS, then add a policy that ties each row to its owner. For a typical "users only see their own rows" table:

-- 1. Turn on Row Level Security for the table
alter table profiles enable row level security;

-- 2. Let a user read only their own row
create policy "Users read own profile"
on profiles for select
using ( auth.uid() = user_id );

-- 3. Let a user write only their own row
create policy "Users write own profile"
on profiles for insert
with check ( auth.uid() = user_id );

The important detail is the using ( auth.uid() = user_id ) line — that's the real WHERE clause doing the work. Avoid using ( true ), which passes every row. If you're using an AI builder, you can paste a prompt like: "Enable RLS on every table that stores user data and add policies so each user can only read and write rows where user_id equals their auth.uid(). Do not use USING(true)."

FAQ

Is the Supabase anon key supposed to be public?

Yes. The anon (anonymous) key is designed to be embedded in your frontend and seen by anyone. Your security comes from RLS policies on your tables, not from hiding this key. The key you must keep secret is the service_role key.

Does enabling RLS break my app?

It can, briefly — once RLS is on, queries return nothing until a policy explicitly allows rows through. That's the system working as intended. You add policies for each access pattern (read own rows, admins read all, etc.) and the app works again, now safely.

How do I know if RLS is actually working and not just "enabled"?

Test as an anonymous and as a different logged-in user. RLS that's enabled but uses a USING (true) policy looks protected in the dashboard but leaks every row. The only reliable check is querying your live endpoints and seeing what comes back.

The bottom line

RLS is a WHERE clause your database enforces on every query, so you can't forget it. For a Supabase app — which is most vibe-coded apps — it's the single most important thing to get right, because the browser talks straight to the database. The anon key being public is fine; tables without a real policy are not. An attacker can find an unprotected table in minutes by reading your network traffic. The point is to find it first, and to keep checking, because your AI ships new tables (and new gaps) every time you add a feature.

Find your gaps before an attacker does.

Is My Site Hackable? scans your deployed app for the exact issues in this article — missing RLS, USING(true) policies, exposed service keys — and tells you what's real and what's a false alarm.

Run a free scan →