Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Set Custom User Roles in Supabase

Custom user roles in Supabase are stored in one of two places: the user's app_metadata field in the JWT token, or a dedicated roles table in your database. To set a role via app_metadata, call supabase.auth.admin.updateUserById() from a server-side function with the service role key. The role then appears in every JWT, accessible via auth.jwt() in RLS policies. For more flexible role management, create a user_roles table and query it in your policies.

What you'll learn

  • How to store roles in app_metadata via the admin API
  • How to create a user_roles table for flexible role assignment
  • How to read roles in RLS policies with auth.jwt()
  • How to assign roles during signup with a database trigger
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read10-15 minSupabase (all plans), @supabase/supabase-js v2+March 2026RapidDev Engineering Team
TL;DR

Custom user roles in Supabase are stored in one of two places: the user's app_metadata field in the JWT token, or a dedicated roles table in your database. To set a role via app_metadata, call supabase.auth.admin.updateUserById() from a server-side function with the service role key. The role then appears in every JWT, accessible via auth.jwt() in RLS policies. For more flexible role management, create a user_roles table and query it in your policies.

Setting Up Custom User Roles in Supabase

Most applications need more than just 'logged in' and 'not logged in'. You might need admin, editor, viewer, or custom roles that control what each user can see and do. Supabase does not have a built-in roles UI, but PostgreSQL and the auth system give you everything needed to implement roles. This tutorial walks through setting up roles, assigning them to users, and reading them in your application and RLS policies.

Prerequisites

  • A Supabase project with Authentication configured
  • At least one test user created
  • Basic understanding of Supabase RLS and auth.uid()

Step-by-step guide

1

Set a role in app_metadata using the admin API

The fastest way to add roles is by storing them in the user's app_metadata. This field is embedded in the JWT token and cannot be modified by the user from the client. Use the admin API from a server-side environment (Edge Function, API route, or script) with the service role key. Once set, the role is available in every authenticated request via auth.jwt().

typescript
1import { createClient } from '@supabase/supabase-js'
2
3// Server-side only — NEVER use service role key in client code
4const supabaseAdmin = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9// Set a user's role to 'admin'
10const { data, error } = await supabaseAdmin.auth.admin.updateUserById(
11 'user-uuid-here',
12 {
13 app_metadata: { role: 'admin' }
14 }
15)
16
17// Set a user's role to 'editor'
18await supabaseAdmin.auth.admin.updateUserById(
19 'another-user-uuid',
20 {
21 app_metadata: { role: 'editor' }
22 }
23)
24
25console.log('Role updated:', data.user.app_metadata)

Expected result: The user's app_metadata now contains { role: 'admin' } and it will appear in their next JWT token.

2

Assign a default role on signup with a trigger

Instead of manually assigning roles to every new user, create a database trigger that automatically sets a default role when a user signs up. This trigger fires after every insert on auth.users and updates app_metadata with the default role. Admins can later upgrade users to higher roles using the admin API.

typescript
1-- Function to set default role on signup
2CREATE OR REPLACE FUNCTION public.set_default_role()
3RETURNS trigger
4LANGUAGE plpgsql
5SECURITY DEFINER
6SET search_path = ''
7AS $$
8BEGIN
9 -- Only set if no role exists yet
10 IF NOT (NEW.raw_app_meta_data ? 'role') THEN
11 NEW.raw_app_meta_data =
12 COALESCE(NEW.raw_app_meta_data, '{}'::jsonb) || '{"role": "viewer"}'::jsonb;
13 END IF;
14 RETURN NEW;
15END;
16$$;
17
18-- Trigger on auth.users insert
19CREATE TRIGGER on_auth_user_created_set_role
20 BEFORE INSERT ON auth.users
21 FOR EACH ROW
22 EXECUTE FUNCTION public.set_default_role();

Expected result: Every new user automatically gets { role: 'viewer' } in their app_metadata without any extra API calls.

3

Read roles in the client application

On the client side, read the user's role from the session to control UI visibility (hide admin buttons from viewers, show edit controls only to editors). Use getUser() or getSession() to access the app_metadata. Remember that client-side role checks are for UI convenience only — the real security enforcement happens in RLS policies.

typescript
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8// Read the user's role
9const { data: { user } } = await supabase.auth.getUser()
10const role = user?.app_metadata?.role ?? 'viewer'
11
12console.log('User role:', role) // 'admin', 'editor', or 'viewer'
13
14// Use in React for UI visibility
15function AdminPanel() {
16 const [role, setRole] = useState<string>('viewer')
17
18 useEffect(() => {
19 supabase.auth.getUser().then(({ data: { user } }) => {
20 setRole(user?.app_metadata?.role ?? 'viewer')
21 })
22 }, [])
23
24 if (role !== 'admin') return null
25 return <div>Admin Panel Content</div>
26}

Expected result: The client application can read the user's role and conditionally render UI elements based on it.

4

Read roles in RLS policies with auth.jwt()

The auth.jwt() function returns the decoded JWT token in SQL, giving you access to app_metadata and the role stored there. Use it in RLS policies to restrict data access by role. This is where the real security enforcement happens — even if someone bypasses your UI, the database itself enforces the role check.

typescript
1-- Read the role from JWT app_metadata in an RLS policy
2CREATE POLICY "Only admins can delete" ON articles
3 FOR DELETE TO authenticated
4 USING (
5 (select auth.jwt()->'app_metadata'->>'role') = 'admin'
6 );
7
8-- Allow multiple roles
9CREATE POLICY "Editors and admins can update" ON articles
10 FOR UPDATE TO authenticated
11 USING (
12 (select auth.jwt()->'app_metadata'->>'role') IN ('editor', 'admin')
13 )
14 WITH CHECK (
15 (select auth.jwt()->'app_metadata'->>'role') IN ('editor', 'admin')
16 );
17
18-- Everyone can read, but only published for non-admins
19CREATE POLICY "Read access by role" ON articles
20 FOR SELECT TO authenticated
21 USING (
22 status = 'published'
23 OR (select auth.jwt()->'app_metadata'->>'role') IN ('editor', 'admin')
24 );

Expected result: RLS policies enforce role-based access at the database level, regardless of what the client sends.

5

Create a roles table for advanced role management

For applications that need multiple roles per user, role hierarchy, or instant role changes, create a dedicated user_roles table. This gives you queryable, auditable role data and avoids the JWT refresh delay. The trade-off is a small performance cost from the extra subquery in RLS policies.

typescript
1-- Create user_roles table
2CREATE TABLE public.user_roles (
3 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4 user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
5 role text NOT NULL CHECK (role IN ('admin', 'editor', 'viewer')),
6 created_at timestamptz DEFAULT now(),
7 UNIQUE(user_id, role)
8);
9
10-- Index for fast lookups in RLS policies
11CREATE INDEX idx_user_roles_user_id ON user_roles (user_id);
12
13-- Enable RLS on the roles table itself
14ALTER TABLE user_roles ENABLE ROW LEVEL SECURITY;
15
16-- Users can read their own roles
17CREATE POLICY "Read own roles" ON user_roles
18 FOR SELECT TO authenticated
19 USING ((select auth.uid()) = user_id);
20
21-- Insert a role
22INSERT INTO user_roles (user_id, role)
23VALUES ('user-uuid', 'editor');

Expected result: A user_roles table that stores role assignments, queryable from RLS policies and application code.

Complete working example

setup-user-roles.sql
1-- ================================================
2-- Custom User Roles Setup for Supabase
3-- ================================================
4
5-- 1. Auto-assign default role on signup
6CREATE OR REPLACE FUNCTION public.set_default_role()
7RETURNS trigger
8LANGUAGE plpgsql
9SECURITY DEFINER
10SET search_path = ''
11AS $$
12BEGIN
13 IF NOT (NEW.raw_app_meta_data ? 'role') THEN
14 NEW.raw_app_meta_data =
15 COALESCE(NEW.raw_app_meta_data, '{}'::jsonb)
16 || '{"role": "viewer"}'::jsonb;
17 END IF;
18 RETURN NEW;
19END;
20$$;
21
22CREATE TRIGGER on_auth_user_created_set_role
23 BEFORE INSERT ON auth.users
24 FOR EACH ROW
25 EXECUTE FUNCTION public.set_default_role();
26
27-- 2. Optional: user_roles table for advanced role management
28CREATE TABLE public.user_roles (
29 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
30 user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
31 role text NOT NULL CHECK (role IN ('admin', 'editor', 'viewer')),
32 created_at timestamptz DEFAULT now(),
33 UNIQUE(user_id, role)
34);
35
36CREATE INDEX idx_user_roles_lookup ON user_roles (user_id, role);
37ALTER TABLE user_roles ENABLE ROW LEVEL SECURITY;
38
39CREATE POLICY "Read own roles" ON user_roles
40 FOR SELECT TO authenticated
41 USING ((select auth.uid()) = user_id);
42
43-- 3. Helper function to check roles from JWT
44CREATE OR REPLACE FUNCTION public.get_my_role()
45RETURNS text
46LANGUAGE sql
47STABLE
48AS $$
49 SELECT COALESCE(
50 auth.jwt()->'app_metadata'->>'role',
51 'viewer'
52 );
53$$;
54
55-- 4. Example RLS policies using JWT roles
56-- Create sample table
57CREATE TABLE articles (
58 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
59 title text NOT NULL,
60 status text DEFAULT 'draft',
61 author_id uuid REFERENCES auth.users(id) NOT NULL,
62 created_at timestamptz DEFAULT now()
63);
64
65ALTER TABLE articles ENABLE ROW LEVEL SECURITY;
66
67CREATE POLICY "Admin full access" ON articles FOR ALL
68 TO authenticated
69 USING ((select public.get_my_role()) = 'admin');
70
71CREATE POLICY "Editor read all" ON articles FOR SELECT
72 TO authenticated
73 USING ((select public.get_my_role()) IN ('editor', 'admin'));
74
75CREATE POLICY "Editor insert own" ON articles FOR INSERT
76 TO authenticated
77 WITH CHECK (
78 (select public.get_my_role()) IN ('editor', 'admin')
79 AND (select auth.uid()) = author_id
80 );
81
82CREATE POLICY "Viewer read published" ON articles FOR SELECT
83 TO authenticated
84 USING (status = 'published');

Common mistakes when setting Custom User Roles in Supabase

Why it's a problem: Storing roles in raw_user_meta_data instead of app_metadata

How to avoid: Use app_metadata for roles. raw_user_meta_data is writable by the user via supabase.auth.updateUser(), so any user could change their own role.

Why it's a problem: Trying to set app_metadata from client-side code

How to avoid: app_metadata can only be set using the admin API with the service role key. Create an Edge Function or server endpoint for role assignment.

Why it's a problem: Relying on client-side role checks for security without RLS policies

How to avoid: Client-side checks (hiding buttons, redirecting) are for UX only. Always enforce roles in RLS policies at the database level where they cannot be bypassed.

Best practices

  • Store roles in app_metadata for simple role models — it is embedded in the JWT with zero query overhead
  • Use a database trigger to auto-assign a default role on signup so no user is ever without a role
  • Always wrap auth.jwt() in a SELECT subquery in RLS policies for per-statement caching
  • Create an Edge Function for role management that verifies the requester is an admin before changing roles
  • Use a roles table when you need multiple roles per user, instant role changes, or audit history
  • Never expose the service role key in client-side code — it bypasses all RLS
  • Default to the most restrictive role (viewer) when no role is assigned

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I want to add admin, editor, and viewer roles to my Supabase project. Show me how to store roles in app_metadata, assign a default role on signup with a trigger, and read the role in RLS policies using auth.jwt().

Supabase Prompt

Create a complete role system for Supabase: a trigger to set a default role on signup in app_metadata, an Edge Function to promote users to admin, and RLS policies on an articles table that restrict access by role.

Frequently asked questions

Where are roles stored in Supabase?

Supabase does not have a built-in roles system. You store roles either in the user's app_metadata field (embedded in the JWT token) or in a custom database table. Both approaches work with RLS policies.

Can I change a user's role from the Dashboard?

Yes. Go to Authentication → Users, find the user, and edit their app_metadata JSON to add or change the role field. The change takes effect on the next token refresh.

How long does it take for a role change to take effect?

With app_metadata, the change is embedded in the JWT which refreshes every hour by default. The user gets the new role on next token refresh. With a roles table, the change is immediate because the database is queried directly.

Can a user have multiple roles simultaneously?

With a roles table, yes — create multiple rows for the same user. With app_metadata, store an array: { roles: ['admin', 'editor'] } and check with a containment operator in policies.

What is the difference between Supabase's anon/authenticated roles and custom roles?

The anon and authenticated roles are PostgreSQL roles that Supabase assigns based on the API key used. Custom roles (admin, editor, viewer) are application-level roles you define and enforce through RLS policies. They work together — custom role checks only apply to authenticated users.

Can RapidDev help set up a role-based access system in Supabase?

Yes. RapidDev can design and implement a custom role system including role assignment flows, RLS policies per table, admin interfaces for role management, and automated testing.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.