The bug I had to fix first
RLS is worth the setup pain, but it has sharp edges. Here's the one that actually happened while building this.
A policy that queried itself
The membership policy checked "does this user belong to this org" by querying the membership table — the same table the policy was protecting. Every read triggered the policy again, which triggered another read.
Postgres said so, plainly
infinite recursion detected in policy for relation "organization_members" — an easy mistake to make, because the rule reads as completely correct in English.
A function that steps outside the check
A security definer function reads membership without re-triggering the policy asking the question — breaking the loop while keeping the rule intact.
-- runs with elevated privileges, bypassing RLS internally create or replace function get_user_org_ids() returns setof uuid language sql security definer set search_path = public as $$ select organization_id from organization_members where user_id = auth.uid() $$;