Supabase RLS Explained: Policies, Examples and Tests
Published Oct 22, 2026 · 7 min read
Contents
Short answer: Supabase Row Level Security (RLS) is a filter the database applies to every request: it only returns or changes the rows a user is allowed to touch. In Supabase, RLS is what protects your data, because the key your app uses in the browser is public. Every table needs RLS on, with one policy per operation.
Why does RLS matter? The CVE-2025-48757 case
The risk is not theoretical. In 2025, a researcher found that 170 of 1,645 Lovable projects had insufficient access rules, which made data readable with nothing more than the public key (Matt Palmer, statement on CVE-2025-48757, May 29, 2025). In almost every case the cause was the same: a table without RLS, or with a rule that was far too broad.
The good news is that this is exactly what well-written policies prevent.
How does Supabase RLS work, in plain English?
Your app asks for "all the files". The database answers "here are the files this user is allowed to see". The filtering happens inside the database, not on the screen. Even someone who bypasses your interface and calls the API directly hits the same filter.
Three basic rules to remember:
- A table with RLS disabled can be read by anyone who has the public key.
- A table with RLS enabled but no policy is closed to everyone.
- An operation without a policy (for example, delete) is refused to everyone.
That last point is useful: if nobody should ever delete a row, simply do not write a delete policy.
What do USING and WITH CHECK do?
A policy is written for one operation: select (read), insert (create), update or delete. It uses up to two clauses:
- USING decides which existing rows are visible or editable. Think of it as the way in.
- WITH CHECK decides whether the new version of a row is acceptable, on insert or after an update. Think of it as the way out.
Missing a WITH CHECK clause is one of the most common mistakes in AI-generated policies.
What do RLS policies look like? Four examples
1. Read your own rows (per user):
create policy "notes_select_own" on public.notes
for select to authenticated
using (user_id = auth.uid());
2. Create rows only for yourself:
create policy "notes_insert_own" on public.notes
for insert to authenticated
with check (user_id = auth.uid());
3. Read rows that belong to your organization (per organization):
create policy "projects_select_org" on public.projects
for select to authenticated
using (private.is_member(org_id));
Here private.is_member is a small helper function that answers one question: is the signed-in user a member of this organization? Keeping it in a private schema stops it from being called directly through the API.
4. Update a file assigned to you, without moving it to another client:
create policy "files_update_assigned" on public.files
for update to authenticated
using (private.is_assigned(client_id))
with check (private.is_assigned(client_id));
Without the WITH CHECK clause, a staff member could edit a file they can see and change its client, attaching it to a client they do not handle.
What are the most common RLS pitfalls?
- The always-true condition. Allowing every signed-in user to read a table keeps anonymous visitors out, but every client sees everything.
- Policies that loop. A policy on the roles table that reads the roles table causes infinite recursion. Use a dedicated security definer function placed in a schema the API does not expose.
- Forgotten join tables. An assignments table readable by everyone reveals who handles which client.
- The role stored in the profile. If users can edit their own profile and their role lives there, they can make themselves admin. Keep roles in a separate table that only admins can change.
- The silent failure. An update blocked by a policy often returns no error, just zero rows changed. Check the number of affected rows in your app and show a clear message.
How do you ask Lovable to write RLS policies?
You do not have to write SQL yourself. Follow these steps:
- Write a permissions table: for each role, what it can read, create, update and delete.
- In Lovable's Plan mode, share that table and ask for a migration that enables RLS on every table and creates one policy per operation, with WITH CHECK on inserts and updates.
- Ask for role checks to be done through a helper function in a private schema, for example one that answers "is the current user an admin?".
- Review the plan with three questions: does every table have RLS enabled in the migration? Do the policies match your permissions table exactly? Are the forbidden operations (often delete) really absent?
- Approve only when all three answers are yes.
The Supabase Security Advisor flags tables without RLS and helper functions that remain callable through the API. Run it after every migration.
How do you test RLS policies?
An untested policy is not a policy. Build a test matrix: one row per table and operation, one column per test profile. Fill in the expected result, then test.
| Operation | Client A | Client B | Assigned staff | Other staff | Admin | Anonymous |
|---|---|---|---|---|---|---|
| Read client A's files | Yes | No | Yes | No | Yes | No |
| Upload to client A's file | Yes | No | Yes | No | Yes | No |
| Change a file's client | No | No | No | No | Yes | No |
| Delete a file | No | No | No | No | No | No |
Then run the tests in three places:
- In the app itself, including by pasting the URL of a page that belongs to another user.
- In the Supabase SQL editor, running queries as a test user.
- With the Supabase Security Advisor, after each migration.
Also index the columns your policies use, such as client_id or org_id. On large tables, a policy on an unindexed column slows down every query.
Where does RLS fit in a wider security review?
RLS is the first of our 12 checks for securing a Lovable app, but not the only one. Secret keys, server-side operations, payment webhooks and private file storage matter too. To see how RLS fits into a complete build, from creating tables to publishing, read our Lovable Supabase tutorial.
The Security Module covers access rules, secrets and security scans in seven lessons, and our free tools include a self-assessment of where your project stands.
FAQ
What was CVE-2025-48757?
A vulnerability published in 2025 affecting Lovable apps whose Supabase access rules were missing or insufficient. According to Matt Palmer's statement of May 29, 2025, 170 of the 1,645 projects examined were affected. The fix is RLS enabled on every table, with policies tested using several accounts.
Should RLS be enabled on every table?
Yes, on every table exposed through the API, including technical tables and join tables. A table with RLS enabled and no policy is simply closed, which is a safe default.
Does RLS slow down the app?
Very little, as long as the columns used in your policies are indexed. Without indexes, the impact grows with table size.
What if a list goes empty after enabling RLS?
That usually means a policy is missing or a linking row is absent, such as an assignment. Never disable RLS to make data reappear. Find the missing policy or row instead.
Can you test RLS without writing SQL?
Mostly, yes. Testing the app with several accounts covers the essentials. The SQL editor then lets you test faster and more systematically.
Sources
- Supabase documentation, Row Level Security, accessed October 22, 2026.
- Matt Palmer, statement on CVE-2025-48757, May 29, 2025, accessed October 22, 2026.
Last updated: October 22, 2026.
Want access rules that hold up in production? See what the Production Program covers.