Demo, all content is generated
Question

RLS for 'members of a team can see the team's projects', how do I write that?

Solved · 845 views · asked by aisha_m · edited

Tables: teams, team_members (team_id, user_id, role), projects (team_id, ...). Users can be in several teams. I want members to see all projects of their teams and only admins to delete. Lovable wrote a policy with a subquery but deleting now fails for everyone, including admins.

What I’ve tried

Tried copying the policy from the Supabase docs example, but that's for single owner rows. Lovable's version is below, select works, delete doesn't.

Comment

3 answers

Marked as helpful by the asker
hannah_reyes · edited

Pattern that scales: one helper function per question, used by every policy.

create schema if not exists private;

create function private.team_role(t uuid)
returns text language sql stable security definer set search_path = ''
as $$
  select role from public.team_members
  where team_id = t and user_id = (select auth.uid())
$$;

create policy "members read projects" on public.projects
  for select to authenticated
  using (private.team_role(team_id) is not null);

create policy "admins delete projects" on public.projects
  for delete to authenticated
  using (private.team_role(team_id) = 'admin');

security definer lets the function read team_members without that table's own RLS getting in the way (otherwise you'll soon meet "infinite recursion detected in policy"). Keep it in a schema that isn't exposed through the API.

Why your delete failed: a delete needs the row to be visible and pass a delete policy. Lovable only created select, so there was no delete policy at all, which means denied.

Comment
There was indeed no delete policy. Used your function, everything works incl. an editor role I added. The 'visible and pass delete' explanation finally made RLS click for me. aisha_m · edited
Good pattern. Add an index on team_members (user_id, team_id) too, the function runs for every row. amir_h · edited
dev_ana · edited

Add a test for this, it's the kind of thing that silently breaks later. Log in as a member of team B in a test and assert that select on team A's project returns nothing and delete affects 0 rows. Deletes blocked by RLS don't throw, they just delete nothing.

Comment
"deletes blocked by RLS don't throw" explains a bug report I got last week aisha_m · edited
noor_builds · edited

saving this, I have the same structure with 'households' instead of teams

Comment