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:

  1. Accounts: User accounts linked to Supabase Auth users
  2. Roles: Authority levels that define what users can do
  3. Permissions: Fine-grained access rights (system or data)
  4. Permission Groups: Optional bundles of related permissions

How they connect:

  1. A user has a Supabase Auth account
  2. That user gets a Supamode account linked to their auth user
  3. The account is assigned one role
  4. The role contains permissions (directly or via groups)
  5. 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 access

Rank 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 accounts
  • role - Creating and editing roles
  • permission - Managing permission definitions
  • auth_user - Managing Supabase Auth users (create, ban, etc.)
  • table - Database table configuration
  • log - Audit log access
  • system_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:

LevelExampleUse Case
Schemapublic.*Full schema access
Tablepublic.blog_postsSingle table access
Columnpublic.users.emailField-level restrictions

Available actions:

  • select - Read data
  • insert - Create records
  • update - Modify records
  • delete - 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 permissions
const editPosts = Permission.createDataPermission({
app,
id: 'edit_posts',
name: 'Edit Blog Posts',
scope: 'table',
schema_name: 'public',
table_name: 'posts',
action: 'update',
});
// 2. Create permission group
const contentGroup = PermissionGroup.create({
app,
id: 'content_group',
config: {
name: 'Content Management',
description: 'Permissions for content editors',
},
});
contentGroup.addPermission(editPosts);
// 3. Create role
const editorRole = Role.create({
app,
id: 'editor_role',
config: {
name: 'Editor',
description: 'Content editor with write access',
rank: 60,
},
});
editorRole.addPermissionGroup({ group: contentGroup });
// 4. Create account
const 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 access
writerAccount.grantPermission({
permission: editAllArticles,
valid_until: new Date('2025-03-31'),
metadata: {
reason: 'Migration assistance',
approved_by: 'editor_in_chief',
},
});
// Revoke specific permission
freelancerAccount.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 broad
const badPermission = Permission.createDataPermission({
name: 'Manage Everything',
action: '*',
table_name: '*',
});
// Better: Specific and scoped
const 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' },
};

Frequently Asked Questions

Can a user have multiple roles?
No. Each Supamode account has exactly one role. If you need combined permissions, create a new role with the required permissions or use permission groups to bundle permissions that multiple roles can share.
What happens if I delete a role that has users assigned?
Users assigned to the deleted role lose all permissions and cannot access Supamode until reassigned to another role. Always reassign users before deleting a role.
What's the difference between permission groups and roles?
Roles define authority levels and are assigned to users. Permission groups are bundles of permissions that can be assigned to roles. One role can have multiple permission groups. Think of groups as reusable permission templates.
How do role ranks work?
Ranks (0-100) define hierarchy. Higher-ranked users can manage lower-ranked accounts but not vice versa. A rank 70 user can assign roles to rank 60 users but cannot modify rank 80 accounts. Use ranks to prevent junior admins from elevating their own privileges.
How do I audit who has access to what?
View all accounts and their roles in the Users Explorer. Each role's permissions are visible in Settings > Permissions. The audit log tracks all permission-related changes.