Understanding Supamode's RBAC System
Complete guide to Supamode's role-based access control system. Learn about accounts, roles, permissions, and permission groups for granular control over admin access.
Supamode's role-based access control (RBAC) system gives you granular control over who can access what in your admin panel. This guide explains how the system works. For practical setup, see Creating Permissions via UI or Permission Templates.
Core Components
The RBAC system consists of four interconnected components:
- Accounts: User accounts linked to Supabase Auth users
- Roles: Authority levels that define what users can do
- Permissions: Fine-grained access rights (system or data)
- Permission Groups: Optional bundles of related permissions
How they connect:
- A user has a Supabase Auth account
- That user gets a Supamode account linked to their auth user
- The account is assigned one role
- The role contains permissions (directly or via groups)
- Permissions define what the user can access
Accounts
Accounts bridge Supabase Auth and Supamode's permission system. Every admin user has exactly one Supamode account.
// Account structure{ id: "uuid-account-id", auth_user_id: "uuid-from-supabase-auth", is_active: true}Accounts with is_active: false cannot perform any actions. Use this to temporarily disable access without deleting the account.
Roles
Roles define authority levels. Each account has exactly one role (or none, meaning no access).
// Role structure{ name: "Content Manager", description: "Manages blog posts and user content", rank: 70}Role Hierarchy
Roles have ranks from 0-100. Higher rank means more authority:
Root (Rank: 100) ← Ultimate system control├── Admin (Rank: 90) ← System administration├── Developer (Rank: 80) ← Technical access├── Manager (Rank: 70) ← Content management├── Support (Rank: 60) ← Customer support└── Viewer (Rank: 50) ← Read-only accessRank rules:
- Users can only manage accounts with lower-rank roles
- A rank 70 user cannot modify a rank 80 account
- This prevents privilege escalation
Supamode does not create default roles. Define a hierarchy that fits your organization.
Permissions
Permissions are the atomic units of access control. Supamode has two permission types:
System Permissions
Control access to Supamode's administrative features:
{ permission_type: "system", name: "Manage User Accounts", system_resource: "account", action: "update", description: "Can modify user account details"}Available system resources:
account- Managing Supamode accountsrole- Creating and editing rolespermission- Managing permission definitionsauth_user- Managing Supabase Auth users (create, ban, etc.)table- Database table configurationlog- Audit log accesssystem_setting- System settings
Grant system permissions only to high-level roles like Admin or Root.
Data Permissions
Control access to your application data:
// Table-level permission{ permission_type: "data", name: "Edit Blog Posts", scope: "table", schema_name: "public", table_name: "blog_posts", action: "update"}// Schema-wide permission (wildcard){ permission_type: "data", name: "Read All Public Data", scope: "table", schema_name: "public", table_name: "*", action: "select"}Granularity levels:
| Level | Example | Use Case |
|---|---|---|
| Schema | public.* | Full schema access |
| Table | public.blog_posts | Single table access |
| Column | public.users.email | Field-level restrictions |
Available actions:
select- Read datainsert- Create recordsupdate- Modify recordsdelete- Remove records*- All actions
Permission Groups
Permission groups bundle related permissions for easier management. They're optional but simplify role configuration.
// Permission group definition{ name: "Content Management", description: "All permissions for content creators", permissions: [ "Read All Blog Posts", "Create Blog Posts", "Edit Own Blog Posts", "Upload Media Files" ]}Benefits:
- Assign multiple permissions at once
- Update group once, all roles using it update
- Keep related permissions together
Example groups:
- Content Management: Blog editing, media upload
- User Administration: Account creation, role assignment
- Customer Support: Ticket access, limited account editing
Security Layers
Supamode implements three security layers:
Layer 1: JWT Token Validation
Fast filtering of unauthorized requests without database queries:
select supamode.check_supamode_access();This checks the JWT's app_metadata for supamode_access: true. Runs on both:
- API side (Hono middleware)
- Database side (before RPC calls)
Layer 2: Database Permission Resolution
Full permission verification with context:
SELECT supamode.has_permission( $current_user_account_id, $permission_id) AS permission_granted;Layer 3: Multi-Factor Authentication (Optional)
When enabled, users must complete MFA before accessing Supamode.
Creating Seed Files
Supamode uses a builder pattern to define permissions programmatically:
import { Account, Permission, PermissionGroup, Role, SupamodeSeedGenerator,} from '../generator';const app = new SupamodeSeedGenerator();// 1. Create permissionsconst editPosts = Permission.createDataPermission({ app, id: 'edit_posts', name: 'Edit Blog Posts', scope: 'table', schema_name: 'public', table_name: 'posts', action: 'update',});// 2. Create permission groupconst contentGroup = PermissionGroup.create({ app, id: 'content_group', config: { name: 'Content Management', description: 'Permissions for content editors', },});contentGroup.addPermission(editPosts);// 3. Create roleconst editorRole = Role.create({ app, id: 'editor_role', config: { name: 'Editor', description: 'Content editor with write access', rank: 60, },});editorRole.addPermissionGroup({ group: contentGroup });// 4. Create accountconst editorAccount = Account.create({ app, id: 'user-uuid-from-supabase-auth',});editorAccount.assignRole(editorRole);export default app;Complete Seed Example
Here's a practical example for a content management system:
import { Account, Permission, PermissionGroup, Role, SupamodeSeedGenerator,} from '../generator';const app = new SupamodeSeedGenerator();// --- ROLES ---const editorInChiefRole = Role.create({ app, id: 'editor_in_chief', config: { name: 'Editor-in-Chief', description: 'Full editorial control', rank: 95, },});const writerRole = Role.create({ app, id: 'staff_writer', config: { name: 'Staff Writer', description: 'Content creator', rank: 60, },});// --- PERMISSIONS ---const createArticles = Permission.createDataPermission({ app, id: 'create_articles', name: 'Create Articles', description: 'Can create new article drafts', scope: 'table', schema_name: 'public', table_name: 'articles', action: 'insert',});const editOwnArticles = Permission.createDataPermission({ app, id: 'edit_own_articles', name: 'Edit Own Articles', description: 'Can edit articles they authored', scope: 'table', schema_name: 'public', table_name: 'articles', action: 'update', conditions: { author_id: '$CURRENT_USER_ID' },});const editAllArticles = Permission.createDataPermission({ app, id: 'edit_all_articles', name: 'Edit All Articles', description: 'Can edit any article', scope: 'table', schema_name: 'public', table_name: 'articles', action: 'update',});const publishArticles = Permission.createDataPermission({ app, id: 'publish_articles', name: 'Publish Articles', description: 'Can change article status to published', scope: 'column', schema_name: 'public', table_name: 'articles', column_name: 'status', action: 'update',});// --- PERMISSION GROUPS ---const editorialGroup = PermissionGroup.create({ app, id: 'editorial_management', config: { name: 'Editorial Management', description: 'Full editorial oversight', },});editorialGroup.addPermissions([ createArticles, editAllArticles, publishArticles,]);const writerGroup = PermissionGroup.create({ app, id: 'content_creation', config: { name: 'Content Creation', description: 'Basic content creation', },});writerGroup.addPermissions([createArticles, editOwnArticles]);// --- ASSIGN GROUPS TO ROLES ---editorInChiefRole.addPermissionGroup({ group: editorialGroup });writerRole.addPermissionGroup({ group: writerGroup });// --- CREATE ACCOUNTS ---const editorAccount = Account.create({ app, id: '550e8400-e29b-41d4-a716-446655440001', config: { metadata: { display_name: 'Sarah Johnson', email: 'sarah@example.com', }, },});editorAccount.assignRole(editorInChiefRole);export default app;Advanced Patterns
Row-Level Conditions
Restrict access based on record data:
const editDraftArticles = Permission.createDataPermission({ app, id: 'edit_draft_articles', name: 'Edit Draft Articles', scope: 'table', schema_name: 'public', table_name: 'articles', action: 'update', conditions: { author_id: '$CURRENT_USER_ID', status: { $in: ['draft', 'revision'] }, },});Time-Limited Roles
Set expiration dates for temporary access:
const tempRole = Role.create({ app, id: 'temp_project_lead', config: { name: 'Project Lead', description: 'Temporary leadership role', rank: 85, valid_from: new Date('2025-10-01'), valid_until: new Date('2025-12-31'), },});Permission Overrides
Grant or revoke permissions for specific accounts:
// Grant temporary elevated accesswriterAccount.grantPermission({ permission: editAllArticles, valid_until: new Date('2025-03-31'), metadata: { reason: 'Migration assistance', approved_by: 'editor_in_chief', },});// Revoke specific permissionfreelancerAccount.denyPermission({ permission: createArticles, valid_until: new Date('2025-02-15'), metadata: { reason: 'Contract suspended', },});Best Practices
Principle of Least Privilege
Grant only the permissions users need:
// Avoid: Too broadconst badPermission = Permission.createDataPermission({ name: 'Manage Everything', action: '*', table_name: '*',});// Better: Specific and scopedconst goodPermission = Permission.createDataPermission({ name: 'Edit Own Articles', action: 'update', table_name: 'articles', conditions: { author_id: '$CURRENT_USER_ID' },});System Permissions for Admins Only
Reserve system permissions (account, role, permission resources) for your highest-level roles. These allow users to create new permissions and roles, so limit them carefully.
Use Permission Groups
Group related permissions together rather than assigning permissions directly to roles. This makes maintenance easier and keeps your permission structure organized.
Always Set Time Limits on Temporary Access
When granting elevated permissions temporarily, always set valid_until:
const tempAccess = { valid_until: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days metadata: { reason: 'Emergency maintenance' },};