Supabase Row Level Security (RLS): A Practical Guide
This article is a practical guide to Supabase Row Level Security (RLS): what it is, why it matters, and how to implement it safely.
Who this is for
- Developers building apps on Supabase (Postgres) who need to secure multi-tenant or user-specific data.
- Anyone who has heard “enable RLS” and wants to understand what to do next.

What is Supabase RLS?
Supabase uses PostgreSQL Row Level Security to control which rows a user can read, insert, update, or delete.
Instead of relying only on your API or frontend logic, RLS enforces rules directly in the database, which makes it much harder to accidentally expose data.
Why RLS matters (a quick example)
Imagine a todos table where multiple users store their tasks.
Without RLS, a logged-in user might be able to query:
select * from todos;
…and see other users’ tasks.
With RLS, you define policies so each user only sees rows they own.
The key idea: policies decide access
RLS works through policies attached to a table.
Policies evaluate per-row conditions using the authenticated user context.
In Supabase, the most common helper is:
auth.uid()which returns the current user’s UUID.
Step 1: Create an example table
Here is a typical todos table:
create table public.todos (
id bigint generated by default as identity primary key,
user_id uuid not null,
title text not null,
is_done boolean not null default false,
created_at timestamptz not null default now()
);
Step 2: Enable RLS on the table
RLS is off by default in Postgres.
Enable it:
alter table public.todos enable row level security;
Optional but recommended for stricter behavior:
alter table public.todos force row level security;
Step 3: Add policies
Read policy (SELECT)
Allow users to read only their own todos:
create policy "Users can read their own todos"
on public.todos
for select
using (auth.uid() = user_id);
Insert policy (INSERT)
Allow users to insert todos only for themselves:
create policy "Users can create their own todos"
on public.todos
for insert
with check (auth.uid() = user_id);
Update policy (UPDATE)
Allow updates only for own rows:
create policy "Users can update their own todos"
on public.todos
for update
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
Delete policy (DELETE)
Allow deletes only for own rows:
create policy "Users can delete their own todos"
on public.todos
for delete
using (auth.uid() = user_id);
Using vs With check (the most common confusion)
using (...)controls which existing rows are visible or targetable.with check (...)controls which rows can be created or what new values are allowed after insert or update.
A useful mental model:
- USING is about “which rows can I touch?”
- WITH CHECK is about “which new row states are allowed?”
Common patterns for Supabase apps
1) User-owned rows
- Keep a
user_idcolumn. - Compare it with
auth.uid().
2) Multi-tenant apps (organization/workspace)
- Add
org_idto each row. - Create a
membershipstable. - Write policies that check membership.
3) Role-based access
- Store roles in a membership table.
- Allow admins broader access via a policy condition.
Debugging and testing RLS
- In the Supabase SQL editor, test policies with realistic queries.
- Verify each of: select, insert, update, delete.
- Test “negative cases” (a user trying to access someone else’s data).
Mistakes to avoid
- Forgetting to enable RLS (policies do nothing if RLS is off).
- Only writing a SELECT policy and assuming writes are also protected.
- Not including WITH CHECK on inserts and updates.
- Relying on the client to set
user_idcorrectly without policy checks.
Wrap-up
Supabase RLS is the most important tool for protecting user data in a Postgres-backed app. Start with simple “user owns row” policies, then evolve toward organization and role-based models as your product grows.