Supabase: how to stop users updating their own role column
Short answer: RLS picks rows, not columns. If your update policy is id = auth.uid() and role sits on profiles, any signed-in user can make themselves an admin. Lock the column with a column grant, or move the role to a table or app_metadata users can't write. To see what your own live site gives away first, run a free SniffPup scan (SniffPup is a free passive security scanner for vibe-coded apps). Only check sites and projects you own.
Home › Guides › Stop users updating their role · Last updated · By Ember, SniffPup
The fast way: a free SniffPup scan runs 15 checks from the outside, including Supabase read access using the public config your site already ships. No signup. Ember can sniff what strangers can reach, but she doesn't sign in as your users, so do the test below too.
A 100 means no issues were found in our checks. It is never proof of security.
1. Why users can edit their own role
This is a very common update policy, and it looks fine:
create policy "Users can update own profile" on public.profiles for update to authenticated using ((select auth.uid()) = id) with check ((select auth.uid()) = id);
Per Supabase's RLS docs, a policy is a filter on rows. It says nothing about columns. So the same user can run this from the browser console with your public key:
await supabase.from("profiles").update({ role: "admin" }).eq("id", myUserId);If your app then checks profiles.role to show the admin panel or in other policies, that user is now an admin. Ember calls this promoting yourself to head of security. Saru does not approve.
2. Fix 1: column-level grants (quickest)
Postgres lets you grant update on specific columns. Supabase documents this as column-level security. Take away table-wide update, then give back only the safe columns:
revoke update on table public.profiles from authenticated, anon; grant update (display_name, avatar_url, bio) on table public.profiles to authenticated;
Keep the RLS update policy from step 1. The grant decides which columns, the policy decides which rows. Any update that touches role now fails with a permission error. Two catches: list every column users really edit, and an upsert also needs insert rights on the columns it sends. Supabase marks column privileges as an advanced feature and, for most apps, recommends a separate roles table instead (Fix 2), so treat this as the fast patch and Fix 2 as the long-term home for roles.
3. Fix 2: move the role somewhere users can't write
Supabase's role-based access guide keeps roles in their own table, like public.user_roles. Turn RLS on and add a select policy if users need to read their own role, but no insert, update or delete policy for users. Only your server (secret key) or SQL changes it.
alter table public.user_roles enable row level security; create policy "Users can read own role" on public.user_roles for select to authenticated using ((select auth.uid()) = user_id);
Or store it in app_metadata, which users can't edit. Never put it in user_metadata: per Supabase's RLS docs, signed-in users can change that themselves (see also user data).
4. Fix 3: a trigger that blocks role changes
If you can't change grants today, a before update trigger can refuse any change to the column unless it comes from your server:
create or replace function public.block_role_change()
returns trigger
language plpgsql
set search_path = ''
as $$
begin
if new.role is distinct from old.role
and current_user in ('authenticated', 'anon') then
raise exception 'role can only be changed by the server';
end if;
return new;
end;
$$;
create trigger profiles_block_role_change
before update on public.profiles
for each row execute function public.block_role_change();A plain with check can't do this on its own, because it only sees the new row. Column grants are simpler, so use the trigger as a backup, not the only lock.
5. Test it as a signed-in user
Sign in as a normal test user in your app, open the browser console and run the update from step 1. It should fail. Then check the row in the Table Editor: the role should not have changed. Also try updating someone else's row: it should change nothing. For more policy patterns, see Supabase RLS policy examples.
FAQ
Why can a user change their own role in Supabase?
RLS policies decide which rows a user can touch, not which columns. A policy like using (id = auth.uid()) on update lets a signed-in user change any column in their own row, so if role or is_admin lives on profiles, they can set it from the browser with the public key.
What's the quickest fix?
Column-level grants: revoke update on public.profiles from authenticated, then grant update only on the safe columns, like grant update (display_name, avatar_url) on public.profiles to authenticated. Keep the RLS update policy too, so users can still only edit their own row.
Where should the role live instead?
Somewhere users can't write: a separate user_roles table with RLS on and no insert, update or delete policy for users, or app_metadata, which users can't edit. Never use user_metadata for roles, because users can edit it themselves.
Can a with check clause stop the role change?
Not cleanly. with check only sees the new row, not the old one. People use a subquery that compares against the stored value, but a column grant or a before update trigger is easier to get right.
Does a clean SniffPup report mean my role column is safe?
No. A score of 100 means no issues were found in our checks. It is never proof of security: an outside scan doesn't sign in as your users, so do the signed-in test on this page too.
Sources: Supabase Row Level Security, Column Level Security, Custom claims and RBAC and User data docs, and the Postgres GRANT and trigger docs. Checked October 31, 2026.
A score of 100 means no issues were found in our checks. It is not proof that your app is secure.