Configure Supabase enterprise SSO, role-based access control, and organization management. Use when implementing SSO integration, configuring role-based permissions, or setting up organization-level...
Supabase supports custom role-based access control (RBAC) by storing role information in app_metadata on the user's JWT, then reading those claims in RLS policies via auth.jwt() ->> 'role'. This skill implements a complete RBAC system: roles in app_metadata, RLS policies that enforce role hierarchies, organization-scoped access, role management through the Admin API, and API endpoints protected with role checks โ all using real createClient from @supabase/supabase-js.
When to use: Building multi-role applications (admin/editor/viewer), implementing organization-scoped access, creating custom permission systems beyond Supabase's built-in anon/authenticated roles, or scoping API operations by user role.
@supabase/supabase-js v2+ with service role key for admin operationsauth.jwt() SQL functionpsql for RLS policy creationThe workflow has three steps: assign roles into the JWT, enforce them in the database with RLS, then enforce them again in application code.
Store custom roles in the user's app_metadata using the Admin API. These claims appear in every JWT the user receives and are readable in RLS policies. Assign roles with the service-role client:
// Define the role hierarchy
type AppRole = 'admin' | 'editor' | 'viewer' | 'member';
// Assign a role to a user (admin operation, service role key required)
async function setUserRole(userId: string, role: AppRole, orgId: string) {
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
app_metadata: { role, org_id: orgId },
});
if (error) throw new Error(`Failed to set role: ${error.message}`);
return data.user;
}
Read the role back in app code with supabase.auth.getUser() and compare it against a numeric hierarchy so hasRole('editor', 'viewer') is true. See role assignment and JWT extraction for the full service-role client setup, granular permissions, bulk assignTeamRoles(), getCurrentUserRole(), getCurrentOrg(), hasRole(), and the requireRole() guard.
Write Row Level Security policies that read auth.jwt() -> 'app_metadata' ->> 'role' and ... ->> 'org_id'. Wrap the JWT extraction in helper functions so every policy stays readable:
CREATE OR REPLACE FUNCTION public.get_user_role()
RETURNS text AS $$
SELECT coalesce(auth.jwt() -> 'app_metadata' ->> 'role', 'viewer');
$$ LANGUAGE sql STABLE SECURITY DEFINER;
CREATE POLICY "editors_create_projects" ON public.projects
FOR INSERT WITH CHECK (
org_id = get_user_org_id()
AND get_user_role() IN ('admin', 'editor')
);
Policies pair an org_id = get_user_org_id() tenant check with a get_user_role() role check per operation (SELECT/INSERT/UPDATE/DELETE). See RLS policies and org-scoped schema for both helper functions, the full policy set across projects/documents/team_members, and the organizations/team_members/projects table schema with indexes.
Enforce roles a second time at the application layer so API routes never rely on RLS alone. See API key scoping and role enforcement for server-side withRole() middleware, per-request client creation, admin panel operations (list members, invite users, change roles), and organization management patterns.
Completing this skill produces:
admin.updateUserById() sets role claims on user JWTsget_user_role() and get_user_org_id() SQL helper functionsorg_id in JWT claims and RLS policieswithRole() middleware for API routes with proper 401/403 responseshasRole() function supporting role escalation comparison| Error | Cause | Solution |
|---|---|---|
app_metadata.role is null in JWT |
Role not set or user needs to re-login | Call admin.updateUserById() to set role; user must refresh their session |
| RLS policy returns empty results | JWT claims don't match policy conditions | Check auth.jwt() output in SQL Editor; verify app_metadata was set correctly |
permission denied for function |
Helper function not created or wrong schema | Create get_user_role() in the public schema with SECURITY DEFINER |
| User role changes not reflected | JWT cached with old claims | User must sign out and sign in again, or call supabase.auth.refreshSession() |
duplicate key value violates unique constraint |
User already in organization | Check team_members table for existing entry before inserting |
foreign key violation on team_members |
User or org doesn't exist | Verify both user_id and org_id exist before inserting membership |
| Role hierarchy bypass | Direct database access with service role | Service role bypasses RLS by design โ restrict its use to server-side admin operations only |
Example 1 โ Quick role check in a component:
async function canEditProject(): Promise<boolean> {
const { data: { user } } = await supabase.auth.getUser();
const role = user?.app_metadata?.role;
return role === 'admin' || role === 'editor';
}
Example 2 โ Verify RLS policies work correctly:
-- Test as an editor in org-123
SET request.jwt.claims = '{"sub": "user-uuid", "role": "authenticated", "app_metadata": {"role": "editor", "org_id": "org-123"}}';
SELECT * FROM projects; -- returns only org-123 rows
INSERT INTO projects (org_id, name, created_by) VALUES ('org-123', 'Test', 'user-uuid'); -- succeeds
DELETE FROM projects WHERE id = 'some-project-id'; -- fails (editors cannot delete)
RESET request.jwt.claims;
For the full onboarding example (onboardOrganization() โ create org, assign creator as admin, seed team_members), see role assignment reference and API scoping reference.
supabase-migration-deep-divesupabase-security-basicssupabase-data-handling