TL;DR. raw_user_meta_data holds whatever the browser passed to supabase.auth.signUp() . On one production app the handle_new_user trigger read the new user's role from it, so anyone could sign up with the Owner role id and own the account. The fix is to take the role from a table that only the server writes, filled before the invite is sent.
This happened on the audit firm time tracker, a Supabase app where every access rule lives in the database. The bug sat in the trigger that creates a profile for each new user.
What did the trigger do?
Simplified, it looked like this:
create function public . handle_new_user () returns trigger language plpgsql security definer set search_path = '' as $$ declare _role_id uuid : = ( new . raw_user_meta_data ->> 'role_id' ):: uuid ; begin insert into public . profiles ( id , full_name , role_id ) values ( new . id , new .β¦