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 guides on fix
- How to fix infinite recursion in Supabase RLS6 min read
- The vibe coded MVP audit checklist8 min read
- Taking over a codebase someone else built6 min read
- Lovable Cloud to Supabase migration, what it actually involves7 min read
- What it actually costs to fix a vibe-coded app6 min read
- Is your Lovable app ready for real users? Ten checks8 min read
- Your Supabase service role key is in your frontend. Do this now5 min read
- Check whether your Supabase data is public in five minutes5 min read
- Why your Lovable app shows a blank page after deploy6 min read