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.

7 min read

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.

About the author

Adeola Ilori founded Adeola Forge and leads its engineering work. The studio builds and fixes web apps, AI-built apps and automations.

More guides on fix

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.

100% code ownership/Direct founder access/Fixed milestones
WhatsApp us