Fix.
Supabase RLS policies that actually work
The four patterns that cover most apps, and why the policy you copied off a forum protects nothing.
Row level security decides, for every single row, whether the person asking is allowed to see or change it. It is the only thing protecting your database, because your Supabase URL and public key are both in your app’s JavaScript.
Most broken setups are not missing policies. They have policies that permit everything.
The policy that protects nothing
create policy "Enable read for all users"
on profiles for select
using (true);
That is a real policy, it shows with a green tick in the dashboard, and it allows every request from anyone. true is always true. This gets added when the app breaks after enabling RLS, because it makes it work again.
A real policy compares something about the request to something about the row.
Pattern 1: each user sees only their own rows
The most common need. The table has a column holding the owner’s user id.
create policy "Users read own rows"
on documents for select
using (auth.uid() = user_id);
auth.uid() is the id of the logged-in user, taken from their token, which they cannot forge. For a table people also write to, add separate policies for insert, update and delete. An insert policy uses with check rather than using:
create policy "Users create own rows"
on documents for insert
with check (auth.uid() = user_id);
Without that check, someone can insert a row claiming any owner they like.
Pattern 2: anyone can read, only the owner can write
For public content with a private author.
create policy "Anyone reads published"
on posts for select
using (published = true);
create policy "Author writes own"
on posts for update
using (auth.uid() = author_id);
Note the read policy still checks something. Only published rows are public, so drafts stay private.
Pattern 3: everyone in a team sees the team’s rows
The one people get wrong most often, because the obvious version causes an infinite loop: the policy on memberships queries memberships, which triggers the policy again.
The fix is a function that runs with the definer’s rights, so it is not subject to the policy:
create function auth_team_ids()
returns setof uuid
language sql
security definer
set search_path = public
stable
as $$
select team_id from memberships where user_id = auth.uid()
$$;
create policy "Team members read team rows"
on projects for select
using (team_id in (select auth_team_ids()));
set search_path is not optional. A security definer function without it is a known escalation route.
Pattern 4: admin access
Never read the role from anything the browser sends. Put it in the user’s app metadata, which only your server can write, or in a table the user cannot update.
create policy "Admins read all"
on invoices for select
using (
(auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'
);
Roles in user_metadata are editable by the user. Roles in app_metadata are not. That single distinction is the whole security boundary.
Test it properly
The dashboard tells you a policy exists, not that it works. Two tests that actually mean something:
The logged-out test. In a private browsing window with no session, try to read the table through the API. Anything that comes back is public.
The other-user test. Create two accounts, add data with each, then try to read the first account’s rows while signed in as the second. If it works, your policy is not filtering by owner.
One thing that will waste your day
Part of your app can exist only in the deployed project and not in your code at all, especially server functions. We hit this on a recruiting platform where six edge functions were deployed but absent from the repository. Any function using the secret key bypasses every policy above, so audit what is actually deployed, not what your code says is there.
More articles on fix
- How to fix infinite recursion in Supabase RLSHow to resolve error 42P17 infinite recursion detected in policy for relation in Supabase when querying team or organization memberships.6 min read
- The vibe coded MVP audit checklistA practical 6-point checklist to find hidden security flaws, exposed secrets, and database performance traps in AI-generated web applications.8 min read
- Taking over a codebase someone else builtWhat to ask before you hand your project to someone new, and why a proper audit before any fix work protects you, not just them.6 min read
Have a project in mind?
Let's build it.
Whether you are building a new SaaS platform from scratch, launching a mobile product, designing a high-conversion interface, or need an urgent rescue for an AI-built app, we are ready to help. You will receive a clear scoped plan and milestone timeline within 1 to 2 business days.