Supabase Auth: Why user_metadata.role Is Not Safe
Putting business roles in user_metadata looks simple, but it is not the right place for serious authorization logic.
Putting a business role in user_metadata seems practical. In a few lines, you store "admin", "manager" or "client" and move forward. The problem is that it's not a good place for serious authorization.
With Supabase, the important distinction is:
rawusermeta_datacan be modified by the userrawappmeta_datacannot be modified by the user
This difference changes everything. If an RLS policy or access logic is based on information that the user can modify himself, it is no longer a real permission. It's a door that you leave ajar for him.
Why the error is common
The diagram is tempting:
- we register a user
- we add a field
roleinuser_metadata - we read this field in the front or in a policy
On paper, it's simple. In practice, this is fragile, because Supabase clearly documents that rawusermeta_data is user-editable with supabase.auth.update().
If you use this field as proof of authorization, you are mixing a convenient profile field with a security rule.
What to use instead
For roles, rights or statuses that must have a security value, the correct zone is rawappmeta_data.
Supabase also explains this in its RLS documentation:
rawusermeta_datais not a good place for authorization datarawappmeta_datais the correct place for this data, because the user cannot modify it himself
Simply said:
user_metadata= profile data, preferences, non-sensitive informationapp_metadata= authorization or access context data
The real risk
The risk is not just theoretical.
Let's imagine a policy like this:
using ((auth.jwt() -> 'user_metadata' ->> 'role') = 'admin')
If a user can change this value, the whole logic becomes questionable. You believe you are filtering access, but you base this filter on an unreliable source.
The problem is even more subtle when the application works "almost". You can have tests that pass, an admin who sees his good screens, and yet a base which is based on information that can be edited by the bad actor.
A healthier structure
The solid version looks more like this:
- you keep the business roles in
rawappmeta_dataor in an internal role table - you read this information in your policies or server controls
- you reserve
user_metadatafor practical fields, such as a name, a language or an avatar
Example of a healthier policy:
using ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')
Or, even better in some cases, you go through a join table that represents your teams, organizations, or access levels. This requires a little more schema, but it is more readable and more scalable.
When user_metadata remains useful
This is not a "bad" field. It is very useful for:
- display a public first or last name
- memorize a language preference
- maintain a time zone
- store a comfort value which has no security impact
The correct test is simple: if the user changes this value, should this give them additional access?
If the answer is no, then user_metadata is fine. If the answer is yes, we must get out of this logic.
The trap of front-end only controls
Many teams also make a similar mistake: they hide a button in the interface if user_metadata.role !== 'admin', then they think that security is taken care of.
This is not security. It's just a display condition.
The forehead may be more comfortable with this type of test. But true control must live:
- in the base with the EPIRB
- or in server code with reliable verification
Otherwise, you just have a pretty curtain in front of a poorly closed door.
A good strategy for a customer portal
For a concrete project, here is a good basis:
user_metadatafor name, avatar, languageapp_metadatafor the global role if you have a simple system- basic business tables if you have organizations, fine-grained permissions or access by client
Typical example:
- a user belongs to one or more organizations
- its real rights are derived from a table
memberships - the front can display a badge or a menu according to these rights
- the base remains the source of truth
This approach avoids overloading the JWT with logic that quickly becomes too rigid.
How to audit an existing project
If you already have a project in place, check three things:
- Does an RLS policy read
user_metadatafor an access decision? - Does a route handler or endpoint server treat
user_metadata.roleas sufficient proof? - Do the front and the base use the same source of truth for rights?
If the answer is yes to the first or second point, there is probably some cleaning to be done.
What to remember
user_metadata is useful for the profile. It is not reliable for authorization.
If a role, status or permission has a security impact, base it on a source that the user cannot modify, such as rawappmeta_data, or better yet on clear business tables and policies.
The right question is not “where is it easiest to store?”. The right question is: who can change this value, and what happens if they change it?
Official sources
Keep reading on the same topic
These links reinforce the blog's topical cluster and help search engines understand the article's primary subject.
