-- Migration 012: atomic create_collective(name, emoji) RPC. -- -- Background: creating a collective via two separate REST calls — -- 1. INSERT INTO collectives ... -- 2. INSERT INTO collective_members (role='admin') ... -- — hits two RLS dead-ends: -- -- a) The first INSERT, when chained with `.select().single()` (the idiomatic -- supabase-js pattern), triggers a SELECT policy check on the returned -- row. The SELECT policy is `is_member(id)`, and the creator isn't a -- member yet — step 2 hasn't happened — so the whole INSERT fails. -- b) The second INSERT is gated by `is_admin(collective_id)`, and nobody -- is admin of the newly-created collective yet, so this also fails. -- -- The intended semantics are "the creator becomes the first admin atomically" -- — which is exactly what a SECURITY DEFINER function is for. -- -- This RPC replaces the two-call pattern in the onboarding flow. It runs as -- the function owner (supabase_admin via search_path = public default grants) -- so it bypasses RLS for both inserts, and returns the new collective row. CREATE OR REPLACE FUNCTION public.create_collective( p_name text, p_emoji text ) RETURNS public.collectives LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_user_id uuid := auth.uid(); v_collective public.collectives%ROWTYPE; BEGIN IF v_user_id IS NULL THEN RAISE EXCEPTION 'unauthenticated' USING ERRCODE = '28000'; END IF; IF p_name IS NULL OR length(btrim(p_name)) = 0 THEN RAISE EXCEPTION 'name_required' USING ERRCODE = '22023'; END IF; INSERT INTO public.collectives (name, emoji, created_by) VALUES (btrim(p_name), coalesce(p_emoji, '🏠'), v_user_id) RETURNING * INTO v_collective; INSERT INTO public.collective_members (collective_id, user_id, role) VALUES (v_collective.id, v_user_id, 'admin'); RETURN v_collective; END; $$; REVOKE ALL ON FUNCTION public.create_collective(text, text) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.create_collective(text, text) TO authenticated;