Postgres Row Level Security: A Supabase Guide

Postgres row level security (RLS) filters every query through rules called policies, so each user sees or changes only the rows they're allowed to.

Gary 路 Published 路 Updated 路 7 min read

Four-step card titled Postgres row level security: grant only what's needed, enable RLS, add one policy per action, test with two accounts.
On this page
  1. What is Postgres row level security?
  2. How do you enable RLS on a Supabase table?
  3. Do grants matter if RLS is on?
  4. How do you write a Supabase RLS policy?
  5. How do you fix "new row violates row-level security policy"?
  6. How do you fix error 42501 after Supabase's Oct 30 change?
  7. What are the most common RLS policy mistakes?
  8. How do you write a team-based policy?
  9. How do you test RLS policies?
  10. FAQ

Short answer: Postgres row level security (RLS) is a database feature that filters every query by rules called policies, so each user only sees or changes the rows they're allowed to. In Supabase, any visitor can call your database with the public key, so RLS on every table with private data is what actually protects it. Turn it on with alter table ... enable row level security, then add one policy per action.

By Gary, Greenlight 路 Last checked against the Supabase and PostgreSQL docs: Oct 20, 2026 路 Drafted with AI tools and reviewed before publishing.

New to the idea? Start with what is Supabase RLS?. This guide is the hands-on version: setup, policy examples and the errors you'll hit.

What is Postgres row level security?

Normally a Postgres role that can read a table can read every row. With row level security on, PostgreSQL docs say "normal access to the table for selecting rows or modifying rows must be allowed by a row security policy." If RLS is on and there's no policy, a default-deny applies: no rows are visible and none can be changed (PostgreSQL docs (external)).

Supabase builds on this. Its browser clients use the public anon or publishable key, which is meant to be public. Supabase's docs say "a table in an exposed schema without RLS is readable and writable by any role with a grant on it," so enable RLS on every table in an exposed schema. Once it's on, no data is reachable through the API with a publishable key until you create policies (Supabase docs (external)).

How do you enable RLS on a Supabase table?

In the SQL editor:

SQL

alter table public.notes enable row level security;

Don't assume a table has RLS on because of how it was created. Run the line for every table you create, ideally in the same migration. Supabase says the Data API exposure badge in the Table Editor and the Security Advisor list the tables worth reviewing (Supabase changelog (external)).

Do grants matter if RLS is on?

Yes. Grants decide whether a role can reach a table at all; policies decide which rows. Supabase's docs warn that "adding policies doesn't take those grants back," so a table protected only by policies can still give anon an insert path if you never revoked the grant. Their pattern is to revoke the client roles' grants, then grant each role only the operations it needs, in the same migration as RLS and policies (Supabase docs (external)):

SQL

alter table public.notes enable row level security;
revoke all on table public.notes from anon, authenticated;
grant select, insert, update, delete on table public.notes to authenticated;

Here signed-out visitors get no grant at all, because they never need notes. Don't blanket-grant everything to anon.

How do you write a Supabase RLS policy?

A policy says which action, for whom, and which rows. using filters rows you read, update or delete; with check validates rows you insert or update (CREATE POLICY (external)). For a notes table with a user_id column:

SQL

-- Read your own notes
create policy "read own notes" on public.notes
for select to authenticated
using ( (select auth.uid()) = user_id );

-- Create notes only as yourself
create policy "insert own notes" on public.notes
for insert to authenticated
with check ( (select auth.uid()) = user_id );

-- Update only your own, and you can't hand it to someone else
create policy "update own notes" on public.notes
for update to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

-- Delete only your own
create policy "delete own notes" on public.notes
for delete to authenticated
using ( (select auth.uid()) = user_id );

Supabase recommends wrapping auth.uid() in (select ...) and adding to authenticated for performance, and indexing the columns your policies use (Supabase docs (external)):

SQL

create index on public.notes (user_id);

Public read, private write (for a blog or product list):

SQL

grant select on table public.posts to anon, authenticated;
create policy "anyone can read posts" on public.posts
for select to anon, authenticated using ( true );

Supabase notes that a using (true) policy for anon "grants every unauthenticated visitor read access to every row the role can already reach through grants," so only use it on data you'd happily publish (Supabase docs (external)).

How do you fix "new row violates row-level security policy"?

This error means an insert or update failed a with check rule, or there's no insert/update policy at all. Work through it in order:

  1. Is there a policy for that action? Select-only policies don't allow inserts.
  2. Is the user signed in? A to authenticated policy rejects anonymous requests, and auth.uid() is null for them.
  3. Is user_id set to the signed-in user? If your frontend forgets to send it, the check auth.uid() = user_id fails. A safer pattern is a column default: alter table public.notes alter column user_id set default auth.uid();
  4. Upserts need both insert and update policies, and often select too.

How do you fix error 42501 after Supabase's Oct 30 change?

Supabase is rolling out a change: on Oct 30, 2026, it applies to all existing projects (new projects got it from May 30). After that, new tables you create in the public schema need an explicit grant before the Data API can reach them. Without one, PostgREST returns error 42501, "permission denied for table", with a hint showing the missing grant. Tables that already exist keep their current grants (Supabase changelog (external)). A grant decides whether a role can reach the table at all; RLS still decides which rows. You need both. Step-by-step fix: Supabase permission denied for table, and how the two layers combine: Supabase GRANT vs RLS. See our Oct 30 change explainer and Oct 30 checker for the exact grant to use.

What are the most common RLS policy mistakes?

  • RLS on, no policies. Everything returns empty and people think the app is broken, then "fix" it with using (true). That reopens the table to everyone. Write the real policy instead.
  • using (true) on private tables. It means "every row, for every role you listed." Fine for public posts, wrong for profiles, orders or messages.
  • Trusting data the user can edit. Supabase's docs warn that raw_user_meta_data "can be updated by the authenticated user", so don't base policies on it (Supabase docs (external)). Use your own table of roles, or raw_app_meta_data.
  • Forgetting with check on update. Without it, a user may be able to change user_id on their own row and move it to someone else.
  • One policy for all. It's legal, but separate policies per action are easier to read and test.
  • Testing with the service role key. It bypasses RLS, so every test passes. Test with real user sessions.

How do you write a team-based policy?

Many apps share rows within a team. Keep a membership table and check it in the policy:

SQL

create policy "team members read projects" on public.projects
for select to authenticated
using (
  team_id in (
    select team_id from public.team_members
    where user_id = (select auth.uid())
  )
);

Put RLS on team_members too, and index team_members (user_id) and projects (team_id). Test it with two accounts on different teams, plus one account on both.

How do you test RLS policies?

Don't trust a policy you haven't tried to break. A simple two-account test:

TestSigned in asExpected
Read notesUser Aonly A's rows
Read notesUser Bonly B's rows
Read notesnot signed in0 rows
Insert note with user_id = BUser Arejected
Update B's noteUser A0 rows changed

Run these from your app or with the Supabase JS client using each user's session, never with the service role key, which bypasses RLS (Supabase docs (external)). If a secret key ever reaches the browser, follow our rotation guide. Building with Lovable? Its RLS steps are in our Lovable launch checklist, and what is vibe coding? explains why AI-built apps so often ship tables without policies.

FAQ

Does RLS slow down queries? It can on large tables. Index policy columns and use the (select auth.uid()) pattern Supabase recommends.

Is the Supabase anon key safe if RLS is on? Yes, it's designed to be public. See is my Supabase anon key safe?

Does the table owner bypass RLS? In plain Postgres, owners and superusers bypass it unless you use force row level security (PostgreSQL docs (external)).

Do views respect RLS? Not by default. Supabase's docs say views bypass RLS by default because they're usually created by the postgres user; on Postgres 15 and above, create them with (security_invoker = true) so the caller's permissions and policies apply (Supabase docs (external); PostgreSQL CREATE VIEW (external)).

Sources

On this site

Related posts

Greenlight is independent and not affiliated with or endorsed by Supabase or PostgreSQL. Product names are used only to identify the tools this guide is about. This page is general information, not professional security advice, legal advice, a security audit or a guarantee. Check your own setup against the vendor's current docs.