- 1Test as a normal userSign in with a test account and try to update your own role from the browser console.
- 2Test the signup pathCall signUp with an extra role value in the metadata and check what lands in profiles.
- 3Restrict which columns users can updateRevoke table-level update and grant update only on harmless columns such as a display name.
- 4Move roles out of user-writable placesKeep roles in a separate table users cannot write to, or in app metadata, never in user metadata.
- 5Guard trusted paths explicitlyDo not treat a missing auth.uid() as proof that a request came from a trusted job.
Graphic: Vibecode Beratung · Based on the sources below
Your app has an admin area, and it checks a role value on the user’s profile. Admins see every customer, normal users see only their own company. The question is simple: can a normal user change that value themselves?
In a recent discussion on Reddit, a developer described exactly this on a multi-tenant app. A new top-level role was added to the allowed values of the role column. The update policy on the profiles table only checked that the row belonged to the user’s own company and never looked at the role. Any signed-in user could promote themselves with one request and see every company in the app. This note explains why that happens, how to test for it and what to fix first.
Why row policies do not protect a role column
Row level security (RLS) decides which rows a request may touch. The Supabase RLS docs describe it as filtering rows, much like an added where clause. It says nothing about which columns inside those rows a user may change.
A policy such as “users can update their own profile” therefore covers every field of that profile, including role, a plan flag or a company ID. A check constraint does not help either. It only decides which values are valid, not who may set them.
Severity: SEV-1. A single request from the browser console can hand a user full access to other tenants’ data. If you have not yet reviewed your policies in general, start with our note on RLS mistakes in Lovable and Bolt apps.
Why with check is not enough
The obvious idea is to add a with check clause to the update policy. The Supabase docs explain that using selects which existing rows can be updated, while with check decides what the resulting row may look like. That is useful for stopping a user from moving a row to another owner.
For a role change, it falls short. As participants in the thread point out, with check only sees the new row. It can say “the role must be one of these values”, but it cannot tell whether the value changed or who changed it.
Two gaps to test today
Imagine a SaaS founder with a Lovable app and a profiles table that holds role. Two tests take a few minutes:
- The update path. Sign in as a normal test user and try to update your own
rolefrom the browser console. If the update succeeds, you have the problem. - The signup path. A common setup copies fields from the signup metadata into
profileswith a trigger on new users. That metadata is whatever the client sent tosignUp(). If the trigger copies a role field, anyone can register as an admin. Test it by signing up with an extra role value and checking the new profile row.
The second gap matters because a guard on updates does not cover inserts. The thread notes that a “before update” trigger never fires at signup.
Fixes, in order
1. Make the column read-only for users. The Supabase column-level security guide shows how. By default, the authenticated role has a table-level update privilege that covers all columns. You revoke that and grant update only on the columns users should edit:
revoke update on table profiles from authenticated;
grant update (display_name, avatar_url) on table profiles to authenticated;
The column names here are examples. Note one trap from the docs: if you keep the table-level privilege and only revoke a column privilege, the table-level one still applies. Put these statements in a migration so they survive the next rebuild. The recent change to explicit grants on new Supabase tables is a good moment to review them anyway.
2. Store roles where users cannot write. The cleaner long-term fix suggested in the thread is a separate table for role assignments that users have no write access to. The Supabase RLS docs add a related rule: raw_user_meta_data can be updated by the signed-in user and is not a place for authorization data, while raw_app_meta_data cannot be updated by the user. In the signup trigger, hardcode the default role and ignore anything role-like in the metadata.
3. If you use a trigger, guard trusted paths explicitly. The original post blocked role changes with a trigger that raises an error unless the caller is already a superadmin, and let seeds and service jobs through because auth.uid() is empty there. A commenter points out the weak spot: requests without a signed-in user also have no auth.uid(). Make the exception for trusted jobs explicit, and add a signed-out test next to the normal-user test.
What this means for your project
If your app stores roles, plans or tenant IDs on a row that users can update, assume the gap exists until a test proves otherwise. Run the two console tests first, because they take minutes and give you a clear answer. Then fix the column grants, move roles out of user-writable places and keep a policy test for each role in your migrations. Our production readiness checklist lists the related checks for auth, secrets and data access.
Authorization bugs like this do not show up in a demo, because every demo user behaves well. A production-readiness review looks at policies, grants and signup triggers together.
Not sure whether a user of your app could promote themselves today? Book a free intro call and describe your setup, and we will tell you honestly whether a review makes sense.
Frequently asked questions
Why does a Supabase update policy not stop users from changing their own role?
An update policy that only checks that the row belongs to the user decides which rows they may change, not which columns. If the role sits in that same row, the user can change it like any other field.
Does with check on an update policy prevent role changes?
Not on its own. The with check clause only sees the new version of the row, so it cannot tell whether the role was changed. It can only say which values the finished row may contain.
Is raw_user_meta_data safe for storing user roles in Supabase?
No. Supabase documents that raw_user_meta_data can be updated by the signed-in user, so it is not a place for authorization data. raw_app_meta_data cannot be updated by the user and is the documented place for that kind of data.
What do column-level privileges do in Supabase?
Row level security controls which rows a role can reach, while column-level privileges control which columns it can use. You can revoke the table-wide update privilege and grant update only on specific columns, so a role column stays read-only for users.