The policy on profiles reads profiles, which runs the policy again, forever. Move the check into a security definer function. It runs as its owner, so its read of profiles doesn't trigger RLS:
create schema if not exists private;
create or replace function private.is_admin()
returns boolean
language sql stable security definer set search_path = ''
as $$
select exists (
select 1 from public.profiles
where id = (select auth.uid()) and role = 'admin'
);
$$;
create policy "Admins can view all profiles" on public.profiles
for select to authenticated
using ((select private.is_admin()));Put it in a schema that isn't exposed over the API (private here), otherwise anyone can call it via RPC. set search_path = '' prevents someone from shadowing profiles with their own table.
And make sure users can't update their own role column, otherwise everyone can make themselves admin.