← Blog
Technical5 min read9 June 2026

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.

PN
Paul Noorman
Founder, OurPlan
Supabase Auth: Why user_metadata.role Is Not Safe

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_data can be modified by the user
  • rawappmeta_data cannot 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 role in user_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_data is not a good place for authorization data
  • rawappmeta_data is the correct place for this data, because the user cannot modify it himself

Simply said:

  • user_metadata = profile data, preferences, non-sensitive information
  • app_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:

  1. you keep the business roles in rawappmeta_data or in an internal role table
  2. you read this information in your policies or server controls
  3. you reserve user_metadata for 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_metadata for name, avatar, language
  • app_metadata for 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:

  1. Does an RLS policy read user_metadata for an access decision?
  2. Does a route handler or endpoint server treat user_metadata.role as sufficient proof?
  3. 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