ASAASA Standard
Not in Phase 1Production Foundation

RBAC & Role Claims Design

Cross-Module · AUTH-18, ADM-05 · Priority: P1

Why It Matters

Most AI-built apps implement roles as a simple binary: admin or user. This means every admin has identical, unlimited access — there's no distinction between "can manage billing" and "can delete all users." When a single admin role controls everything, one compromised account means total access.

Proper RBAC (Role-Based Access Control) separates permissions by function — billing manager, support agent, super admin — so each role only has access to what it needs. SOC 2 compliance requires least-privilege access and documented role definitions.

Priority: P1 — Required for multi-user teams and enterprise readiness.

Affected Stack: Supabase Auth, Next.js, any framework with role-based access


AUTH-18: RBAC in app_metadata

The Problem

AI tools store roles in user_metadata — which users can modify themselves through Supabase's client API. Or worse, roles are stored in a profiles table without RLS, making them editable by any authenticated user.

// ❌ Role in user_metadata — user can self-promote
const { data } = await supabase.auth.updateUser({
  data: { role: 'admin' } // User calls this themselves!
});

The Fix

Store roles in app_metadata, which can only be modified by the service role (server-side). Users cannot change their own app_metadata.

// ✅ Set role via service role only (server-side)
const { error } = await supabaseAdmin.auth.admin.updateUserById(userId, {
  app_metadata: { role: 'billing_manager' }
});
// ✅ Read role from app_metadata (cannot be tampered with)
const { data: { user } } = await supabase.auth.getUser();
const role = user?.app_metadata?.role; // 'admin', 'billing_manager', 'support'

ADM-05: RBAC Beyond Binary

The Problem

Binary admin/user roles create two issues:

  1. Over-privileged admins — every admin can do everything
  2. No growth path — when you need "support can view users but not delete" or "billing can manage subscriptions but not access admin settings," binary roles can't express it

The Fix

Design roles around job functions, not access levels.

// ✅ Role hierarchy with specific permissions
const ROLE_PERMISSIONS: Record<string, string[]> = {
  super_admin: ['*'],  // Full access
  billing_manager: [
    'billing.view', 'billing.modify', 'subscriptions.manage',
    'users.view' // Can view users but not modify
  ],
  support: [
    'users.view', 'users.impersonate',
    'tickets.manage'
  ],
  member: [
    'own_data.view', 'own_data.modify'
  ],
};

// ✅ Permission check helper
function hasPermission(role: string, permission: string): boolean {
  const perms = ROLE_PERMISSIONS[role] || [];
  return perms.includes('*') || perms.includes(permission);
}
// ✅ Use in API route
if (!hasPermission(user.app_metadata.role, 'users.delete')) {
  return new Response('Forbidden', { status: 403 });
}

Key rules:

  • Define roles based on job functions, not technical access levels
  • Store role definitions in a shared config (not scattered across components)
  • Use app_metadata for role storage (not user_metadata or client-editable tables)
  • Implement least-privilege: start with no access, grant only what's needed
  • Document role permissions for SOC 2 compliance

References


Related Checks


Is your app safe? Run Free Scan →