-- Private-by-default follow model + mutuals enforcement -- 1) Profiles: add privacy/official flags alter table if exists profiles add column if not exists is_private boolean not null default true, add column if not exists is_official boolean not null default false; -- 2) Follows: add status and constraint alter table if exists follows add column if not exists status text not null default 'accepted'; do $$ begin if not exists ( select 1 from pg_constraint where conname = 'follows_status_check' ) then alter table follows add constraint follows_status_check check (status in ('pending', 'accepted')); end if; end $$; -- 3) Request follow function (privacy-aware) create or replace function request_follow(target_id uuid) returns text language plpgsql security definer as $$ declare existing_status text; target_private boolean; target_official boolean; new_status text; begin if auth.uid() is null then raise exception 'Not authenticated'; end if; select status into existing_status from follows where follower_id = auth.uid() and following_id = target_id; if existing_status is not null then return existing_status; end if; select is_private, is_official into target_private, target_official from profiles where id = target_id; if target_private is null then raise exception 'Target profile not found'; end if; if target_official or target_private = false then new_status := 'accepted'; else new_status := 'pending'; end if; insert into follows (follower_id, following_id, status) values (auth.uid(), target_id, new_status); return new_status; end; $$; -- 4) Mutual follow must be accepted on both sides create or replace function is_mutual_follow(user_a uuid, user_b uuid) returns boolean language plpgsql security definer as $$ begin return exists ( select 1 from follows f1 where f1.follower_id = user_a and f1.following_id = user_b and f1.status = 'accepted' ) and exists ( select 1 from follows f2 where f2.follower_id = user_b and f2.following_id = user_a and f2.status = 'accepted' ); end; $$; -- 5) Follow request management helpers create or replace function accept_follow_request(requester_id uuid) returns void language plpgsql security definer as $$ begin if auth.uid() is null then raise exception 'Not authenticated'; end if; update follows set status = 'accepted' where follower_id = requester_id and following_id = auth.uid(); end; $$; create or replace function reject_follow_request(requester_id uuid) returns void language plpgsql security definer as $$ begin if auth.uid() is null then raise exception 'Not authenticated'; end if; delete from follows where follower_id = requester_id and following_id = auth.uid(); end; $$; create or replace function get_follow_requests() returns table ( follower_id uuid, handle text, display_name text, avatar_url text, requested_at timestamptz ) language sql security definer as $$ select f.follower_id, p.handle, p.display_name, p.avatar_url, f.created_at as requested_at from follows f join profiles p on p.id = f.follower_id where f.following_id = auth.uid() and f.status = 'pending' order by f.created_at desc; $$; -- 6) Posts RLS: allow self, public, or accepted follow alter table if exists posts enable row level security; drop policy if exists posts_select_private_model on posts; create policy posts_select_private_model on posts for select using ( auth.uid() = author_id or exists ( select 1 from profiles p where p.id = author_id and p.is_private = false ) or exists ( select 1 from follows f where f.follower_id = auth.uid() and f.following_id = author_id and f.status = 'accepted' ) );