BOSS implements a comprehensive Role-Based Access Control (RBAC) system using Supabase's native Custom Claims and Auth Hooks. This system provides secure, scalable role management with minimal overhead and maximum flexibility for plugin extensions.
Roles form a hierarchy (a DAG) where a parent role inherits the effective permissions of its children. Inheritance, permission-based plugin gating, and grant delegation are all derived from this single tree.
admin ← inherits everything below (full access)
/ \
boss_admin finance_admin ← each inherits user
| \ |
boss_plugin_admin \ | ← plugin authoring only
| \ |
+------------ user ← baseline (legacy plugins assume this)
boss_admin --> user is kept alongside boss_admin --> boss_plugin_admin --> user;
the duplicate path is harmless because get_role_descendants uses UNION.
boss_plugin_admin is the least-privilege plugin author: plugins.create +
api_key.create, plus the inherited user.* baseline. It deliberately holds no
plugins.admin.* moderation permission and no role/finance/RPA verb. Note it cannot
assign any role despite user being a strict descendant - assign_role_to_user
requires role.assign first, which this role does not have. See migration
20260731000000_plugin_create_permission.sql. (It held no secret.read either until
20260809000000 put that on the user baseline; it now inherits it like everyone
else, and still holds no secret.share.role.)
The user baseline carries user.read/write/update/delete,
organisation.create, organisation.read and secret.read. Each of those is a
deliberate kill switch: revoking one from user disables that capability
deployment-wide without touching any other role. secret.read is what makes the
Secret Manager panel - and with it the AI section that plugin owns -
available to ordinary users. It was admin-only until 20260809000000; see that
migration's header for why that was a leftover from the pre-RBAC requiresAdmin flag
rather than a security posture. The vault's RPCs are granted to authenticated and
self-scope with auth.uid() regardless of the permission.
secret.share.role (admin + boss_admin) is the one secret capability that is not
everyone's. A role share reaches every holder of that role, and user is a descendant
of every role, so a role target publishes a credential deployment-wide with no undo.
share_secret enforces it; the Secret Manager panel hides the Roles tab without it.
Sharing with an individual user is ungated, and sharing with an organisation requires
membership of that organisation instead.
- Effective permissions - a user's permissions = the union of permissions on each assigned role plus all of that role's descendants. Computed by
get_role_descendants(role_id)(recursive, cycle-safe) andget_effective_permissions(user_id).authorize(permission)walks this closure, so a permission granted touseris automatically held byboss_admin,finance_admin, andadmin. - JWT claim -
custom_access_token_hookinjects the effective set as theuser_permissionsclaim (for both magic-link and passkey logins). The client parses it intoRoleClaims.permissions/UserInfo.permissions(UI/visibility only - server still enforces). - Permission-based plugin gating -
plugin.jsondeclaresrequiredPermissions: [...]; the host (DynamicPluginManager.canAccess) shows/registers a plugin only if the user's effective permissions contain all of them. An empty list (and legacy plugins) means "any authenticated user".requiresAdminis the legacy gate, still honored. Example: Admin: Create Roles requires["role.read","role.create"]; Admin: Roles requires["role.read","role.assign"]. - Delegated grants (tree-derived) - a non-admin grantor may assign only roles strictly below its own roles in the tree.
get_grantable_roles()returns that set (used to populate the assignment dropdown) andassign_role_to_user/remove_role_from_userenforce it server-side. Full admins may assign any role. Consequence:boss_admincannot assign its siblingfinance_admin- by construction, with no hand-maintained matrix. To let a role grant more, hang those roles beneath it inrole_hierarchy. - Tables/functions - new table
role_hierarchy(parent_role_id, child_role_id); new permissionrole.read; the role-management RPCs (create_new_role,get_all_roles,delete_role, …) are gated byauthorize('role.<verb>')instead of a hardis_user_admin()check, so domain admins work without holding the literaladminrole. See migration20260625000000_role_hierarchy_and_granular_rbac.sql.
- Admins bypass
authorize().authorize()short-circuits totruefor any user with theadminrole, so the server agrees with the client (UserInfo.hasPermission/DynamicPluginManager.canAccessalso bypass for admins) even for permissions outside admin's role closure (e.g. a brand-new permission a plugin introduces). Non-admins are evaluated purely against their inherited permission closure. role.updatedelegation is constrained.assign_permission_to_role/remove_permission_from_rolerequirerole.update. Admins may attach any permission to any role. A non-admin holder ofrole.update(none today) may only modify roles strictly below it (get_grantable_role_ids) and may only attach permissions it itself holds (authorize(permission_name)) - so a delegated role-manager can never mint an admin-level role. This is enforced in the RPCs, not just documented.- Listing all users requires
role.read(admin + boss_admin), notuser.read. The baselineuserrole carriesuser.read, so gating the all-users list on it would expose every user to everyone. Plain users still read only their own row. - Refresh latency (stale JWT). Effective permissions are baked into the access token at issuance. Gaining and losing a role/permission take effect only on the next token refresh (≤
jwt_expiry, default 1h) - a revoked permission lingers in the existing token and in the JWT-claimusersRLS policy until then; force a sign-out / token invalidation for immediate revocation. (A brand-new signup does get its baselineuser.*on the first token, becausehandle_new_userassigns theuserrole before the token is minted.) - Helper exposure.
get_role_descendants,get_effective_permissions, andget_grantable_role_idsareSECURITY DEFINERinternals not granted toanon/authenticated(onlysupabase_auth_adminfor the hook); the only public surface is the self-scopedget_grantable_roles().
AuthDataProvider.hasPermission(name) / hasAnyPermission(...) and the userPermissions StateFlow now carry effective permissions (e.g. role.create, user.read), not role names. Previously they carried role names (hasPermission("admin") style checks). Plugins that passed a role name to hasPermission or read userPermissions expecting role names must migrate to permission names (or use currentUser/isAdmin for role checks). The in-tree plugins are unaffected (they use supabaseDataProvider.rpc); third-party plugins should be advised in release notes.
Known limitation - domain-admin role creation needs admin follow-up. A
boss_admin(which holdsrole.create) can create a role, but cannot place it in the hierarchy (role_hierarchyedits are admin-only), grant it (it is not a descendant), or attach permissions (role.updateis admin-only). Such a role is inert until anadminwires it up.
- Database Layer - PostgreSQL tables with UUID-based relationships, RLS policies, and functions
- Auth Hooks - JWT claim injection for Supabase native auth (magic link, OAuth)
- Edge Functions - Custom JWT generation for passkey authentication with RBAC claims
- Kotlin Services - Client-side role management and checking
- JWT Integration - Role claims embedded in access tokens (both auth methods)
User Signs Up
↓
handle_new_user() trigger → Assigns 'user' role
↓
User Authenticates
↓
custom_access_token_hook() → Injects role claims into JWT
↓
Client receives JWT with claims
↓
RoleService parses claims → Available to application
↓
RLS policies enforce permissions at database level
BOSS supports multiple authentication methods, and RBAC role claims are consistently included across all of them:
- User requests magic link via email
- Supabase Auth validates the link
- Auth Hook (
custom_access_token_hook) runs before JWT issuance - Hook fetches user roles from database
- JWT issued with
user_role,user_roles, andis_adminclaims
- User authenticates with Touch ID/Windows Hello/Security Key
- Edge Function (
/functions/v1/passkey) verifies signature - Edge Function fetches user roles from
user_rolestable - Custom JWT generated with RBAC claims included
- Client imports session with full role information
Key Point: Both methods produce identical JWT claim structures, ensuring consistent RBAC behavior regardless of authentication method.
JWT Claims Structure (Both Methods):
{
"sub": "user-uuid",
"email": "user@example.com",
"role": "authenticated",
"user_role": "user", // Primary role
"user_roles": ["user", "admin"], // All roles
"is_admin": true // Admin flag
}Defines available roles in the system (fully dynamic).
CREATE TABLE public.roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
description TEXT,
is_system BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);System Roles:
user- Default role for all users (is_system=true, cannot be deleted)admin- Administrative role with full permissions (is_system=true, cannot be deleted)
Extensibility: Add custom roles dynamically via RoleCreationService:
RoleCreationService.createRole("ai_trainer", "AI Training Team Member")Defines granular permissions (fully dynamic).
CREATE TABLE public.permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
description TEXT,
is_system BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);System Permissions:
users.read,users.writeworkspaces.read,workspaces.write,workspaces.deleteplugins.install,plugins.manageadmin.access
Maps users to their assigned roles (many-to-many relationship).
CREATE TABLE public.user_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
role_id UUID REFERENCES public.roles(id) ON DELETE CASCADE,
assigned_by UUID REFERENCES auth.users(id),
assigned_at TIMESTAMPTZ DEFAULT now(),
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (user_id, role_id)
);Fields:
id- Unique identifieruser_id- Reference to user in auth.usersrole_id- Reference to role in roles table (UUID foreign key)assigned_by- Admin who assigned the role (audit trail)assigned_at- When the role was assigned (audit trail)
Maps roles to specific permissions for fine-grained access control.
CREATE TABLE public.role_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
role_id UUID REFERENCES public.roles(id) ON DELETE CASCADE,
permission_id UUID REFERENCES public.permissions(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (role_id, permission_id)
);data class RoleClaims(
val userRole: String, // Primary role name
val userRoles: List<String>, // All role names
val isAdmin: Boolean // Quick admin check
) {
fun hasRole(role: String): Boolean
fun hasAnyRole(vararg roles: String): Boolean
fun hasAllRoles(vararg roles: String): Boolean
}data class UserInfo(
val id: String,
val email: String,
val createdAt: String,
val roleClaims: RoleClaims? = null
) {
val primaryRole: String // Returns role name (e.g., "admin")
val roles: List<String> // Returns list of role names
val isAdmin: Boolean
fun hasRole(role: String): Boolean
}data class UserWithRoles(
val userId: String,
val email: String,
val roles: List<String>, // Role names
val isAdmin: Boolean
) {
val primaryRole: String // First role or "user"
}Core service for role management operations.
object RoleService {
// Parse role claims from JWT session
fun parseRoleClaimsFromSession(session: UserSession?): RoleClaims?
// Get user roles
suspend fun getUserRoles(userId: String): Result<List<UserRole>>
// Check if user has role
suspend fun userHasRole(userId: String, roleName: String): Result<Boolean>
suspend fun isUserAdmin(userId: String): Result<Boolean>
// Assign/remove roles (admin only)
suspend fun assignRoleByName(targetUserId: String, roleName: String): Result<Unit>
suspend fun removeRoleByName(targetUserId: String, roleName: String): Result<Unit>
// Permission checking
suspend fun getRolePermissions(roleName: String): Result<List<RolePermission>>
suspend fun canPerformAction(userId: String, permissionName: String): Result<Boolean>
}Service for creating and managing roles and permissions dynamically.
object RoleCreationService {
// Role management
suspend fun createRole(name: String, description: String?): Result<Unit>
suspend fun getAllRoles(): Result<List<RoleInfo>>
suspend fun deleteRole(roleName: String): Result<Unit>
// Permission management
suspend fun createPermission(name: String, description: String?): Result<Unit>
suspend fun getAllPermissions(): Result<List<PermissionInfo>>
suspend fun deletePermission(permissionName: String): Result<Unit>
// Role-Permission mapping
suspend fun assignPermissionToRole(roleName: String, permissionName: String): Result<Unit>
suspend fun removePermissionFromRole(roleName: String, permissionName: String): Result<Unit>
}object AuthService {
// Current user role checking
fun getCurrentUserRoleClaims(): RoleClaims?
fun isCurrentUserAdmin(): Boolean
fun currentUserHasRole(roleName: String): Boolean
// Role management (proxies to RoleService)
suspend fun assignRoleByName(targetUserId: String, roleName: String): Result<Unit>
suspend fun removeRoleByName(targetUserId: String, roleName: String): Result<Unit>
suspend fun getUserRoles(userId: String): Result<List<UserRole>>
suspend fun userHasPermission(userId: String, permissionName: String): Result<Boolean>
}import ai.rever.boss.services.supabase.AuthService
// Simple admin check
if (AuthService.isCurrentUserAdmin()) {
println("User is an admin")
}
// Check specific role
if (AuthService.currentUserHasRole("admin")) {
println("User has admin role")
}
// Check custom role
if (AuthService.currentUserHasRole("ai_trainer")) {
println("User is an AI trainer")
}
// Get all role claims
val claims = AuthService.getCurrentUserRoleClaims()
claims?.let {
println("Primary role: ${it.userRole}") // e.g., "admin"
println("All roles: ${it.userRoles}") // e.g., ["user", "admin", "ai_trainer"]
println("Is admin: ${it.isAdmin}")
}import ai.rever.boss.services.supabase.AuthService
import kotlinx.coroutines.flow.collectLatest
// Observe user changes
AuthService.currentUser.collectLatest { user ->
user?.let {
println("User: ${it.email}")
println("Primary role: ${it.primaryRole}") // String role name
println("Is admin: ${it.isAdmin}")
if (it.hasRole("admin")) {
// Show admin UI
}
if (it.hasRole("ai_trainer")) {
// Show AI trainer features
}
}
}import ai.rever.boss.services.supabase.AuthService
// Assign system role
val result = AuthService.assignRoleByName(
targetUserId = "user-uuid",
roleName = "admin"
)
result.fold(
onSuccess = { println("Role assigned successfully") },
onFailure = { error -> println("Failed to assign role: ${error.message}") }
)
// Assign custom role
val customResult = AuthService.assignRoleByName(
targetUserId = "user-uuid",
roleName = "ai_trainer"
)val result = AuthService.removeRoleByName(
targetUserId = "user-uuid",
roleName = "admin"
)
result.fold(
onSuccess = { println("Role removed successfully") },
onFailure = { error -> println("Failed to remove role: ${error.message}") }
)import ai.rever.boss.services.supabase.RoleCreationService
val result = RoleCreationService.createRole(
name = "ai_trainer",
description = "AI Training Team Member"
)
result.fold(
onSuccess = { println("Role created successfully") },
onFailure = { error -> println("Failed to create role: ${error.message}") }
)import ai.rever.boss.services.supabase.AuthService
val canManagePlugins = AuthService.userHasPermission(
userId = "user-uuid",
permissionName = "plugins.manage"
)
canManagePlugins.fold(
onSuccess = { hasPermission ->
if (hasPermission) {
// Allow plugin management
}
},
onFailure = { /* Handle error */ }
)@Composable
fun AdminPanel() {
val currentUser by AuthService.currentUser.collectAsState()
if (currentUser?.isAdmin == true) {
Column {
Text("Admin Panel")
Button(onClick = { /* Admin action */ }) {
Text("Manage Users")
}
}
} else {
Text("Access Denied")
}
}Assigns a role to a user by role name. Only callable by admins.
SELECT public.assign_role_to_user(
'user-uuid'::uuid,
'admin'::text
);Removes a role from a user by role name. Only callable by admins. Cannot remove own admin role.
SELECT public.remove_role_from_user(
'user-uuid'::uuid,
'admin'::text
);Check if a user has a specific role by role name.
SELECT public.check_user_has_role(
'user-uuid'::uuid,
'admin'::text
);Quick check if a user is an admin.
SELECT public.is_user_admin('user-uuid'::uuid);Get all roles for a user.
SELECT * FROM public.get_user_roles('user-uuid'::uuid);Check if current user (from JWT) has a permission. Used in RLS policies.
-- In RLS policy
CREATE POLICY "Admins can delete" ON workspaces
FOR DELETE
USING (public.authorize('workspaces.delete'));Users can view their own roles:
CREATE POLICY "Users can view their own roles"
ON public.user_roles
FOR SELECT
USING (auth.uid() = user_id);Admins can view all roles:
CREATE POLICY "Admins can view all roles"
ON public.user_roles
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM public.user_roles
WHERE user_id = auth.uid() AND role = 'admin'
)
);Only admins can assign roles:
CREATE POLICY "Admins can assign roles"
ON public.user_roles
FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM public.user_roles
WHERE user_id = auth.uid() AND role = 'admin'
)
);-- Example: Only users with 'workspaces.delete' permission can delete
CREATE POLICY "Authorized users can delete workspaces"
ON public.workspaces
FOR DELETE
USING (public.authorize('workspaces.delete'));# Navigate to project root
cd /path/to/boss-main
# Deploy migrations using Supabase CLI
supabase db push
# Or apply migrations manually in Supabase Dashboard
# SQL Editor → New Query → Paste migration content → Run- Go to Supabase Dashboard
- Navigate to Authentication → Hooks
- Enable Custom Access Token Hook
- Select:
public.custom_access_token_hook - Save changes
Since the first admin must be created manually (chicken-and-egg problem):
-- Find your user ID
SELECT id, email FROM auth.users WHERE email = 'your-email@example.com';
-- Assign admin role
INSERT INTO public.user_roles (user_id, role)
VALUES ('your-user-uuid', 'admin');After this, the admin can assign roles to other users via the application.
// In your app initialization
suspend fun testRBAC() {
// Sign in
AuthService.sendMagicLink("test@example.com")
// After authentication
val claims = AuthService.getCurrentUserRoleClaims()
println("Role: ${claims?.userRole}") // Should print: Role: user or Role: admin
// For admins
if (AuthService.isCurrentUserAdmin()) {
val result = AuthService.assignRoleByName("other-user-id", "admin")
println("Assign result: $result")
}
}Plugins can create roles at runtime without database migrations or code changes:
- Create plugin-specific role:
import ai.rever.boss.services.supabase.RoleCreationService
// Create roles for analytics plugin
RoleCreationService.createRole(
name = "analytics_viewer",
description = "Can view analytics dashboards"
)
RoleCreationService.createRole(
name = "analytics_manager",
description = "Can view and manage analytics"
)- Add plugin permissions:
// Create permissions
RoleCreationService.createPermission(
name = "plugin.analytics.view",
description = "View analytics data"
)
RoleCreationService.createPermission(
name = "plugin.analytics.manage",
description = "Manage analytics settings"
)
// Map permissions to roles
RoleCreationService.assignPermissionToRole(
roleName = "analytics_viewer",
permissionName = "plugin.analytics.view"
)
RoleCreationService.assignPermissionToRole(
roleName = "analytics_manager",
permissionName = "plugin.analytics.view"
)
RoleCreationService.assignPermissionToRole(
roleName = "analytics_manager",
permissionName = "plugin.analytics.manage"
)- Use in plugin code:
// Check if user can access plugin
if (AuthService.currentUserHasRole("analytics_viewer")) {
// Show analytics dashboard
}
// Or check permission
val canManage = AuthService.userHasPermission(
userId = currentUserId,
permissionName = "plugin.analytics.manage"
)
canManage.fold(
onSuccess = { hasPermission ->
if (hasPermission) {
// Show management UI
}
},
onFailure = { /* Handle error */ }
)The Admin Role Management plugin provides a UI for:
- Viewing all users with their roles
- Assigning/removing roles (including custom roles)
- Creating new roles and permissions
- Managing role-permission mappings
All role management operations are available at runtime without code changes.
The client does NOT verify JWT signatures. This is intentional and follows industry best practices:
- JWT Already Verified Server-Side: Supabase Auth verifies signatures when issuing tokens
- Client Cannot Be Trusted: Any code running on the client can be modified/debugged
- Performance: Signature verification is expensive and unnecessary on client
- Security Happens Server-Side: RLS policies enforce all authorization
✅ Safe Uses:
- Showing/hiding UI elements (buttons, menu items)
- Displaying role badges and user info
- Optimistic UI updates
- Reducing unnecessary API calls
❌ NEVER Use For:
- Granting access to sensitive data
- Bypassing server-side authorization
- Making security decisions
- Skipping database permission checks
Client Side Server Side
┌──────────────────┐ ┌──────────────────────┐
│ Parse JWT Claims │ │ Verify JWT Signature │
│ (informational) │ → API → │ Check RLS Policies │
│ │ Call │ Execute if Authorized│
└──────────────────┘ └──────────────────────┘
UI Only Actual Security
Example: Admin Role Check
// Client side (UI convenience)
if (AuthService.isCurrentUserAdmin()) {
// ✅ Show "Delete User" button
// ❌ DO NOT skip server call
}
// Server side (actual security)
// RLS Policy on user_roles table:
CREATE POLICY "Only admins can delete users" ON auth.users
FOR DELETE USING (
public.is_user_admin(auth.uid()) -- Verifies JWT claims server-side
);Key Principle: Client-side checks are optimistic assumptions. Server-side checks are enforced guarantees.
- Users cannot remove their own admin role (prevents lockout)
- First admin must be created manually via SQL
- All role changes are audited (assigned_by, assigned_at)
- Role claims are signed in JWT by Supabase (server-side)
- Signature verification happens server-side on every API request
- Client parsing is informational only - does NOT verify signature
- Claims refresh automatically on token renewal (every hour)
- Forged JWTs are rejected by Supabase API (signature mismatch)
- Server-side RLS policies check
auth.jwt()claims (verified)
- All tables with roles/permissions have RLS enabled
- Service role bypasses RLS (for Edge Functions only)
- Users can only see their own roles
- Admins can view/modify all roles (verified server-side)
- RLS policies use
auth.jwt()which contains verified claims
- Always use
authorize()function in RLS policies - Never trust client-side role checks alone
- Database enforces permissions regardless of client claims
- All mutations protected by RLS + database functions
- Edge Functions use service role to bypass RLS (trusted environment)
For Magic Link Authentication:
-
Check auth hook is enabled:
- Dashboard → Authentication → Hooks → Custom Access Token Hook
-
Verify role assignment:
SELECT * FROM public.user_roles WHERE user_id = 'your-user-uuid';
-
Test hook function directly:
SELECT public.custom_access_token_hook( jsonb_build_object( 'user_id', 'your-user-uuid', 'claims', '{}'::jsonb ) );
For Passkey Authentication:
-
Verify Edge Function is deployed:
supabase functions deploy passkey --project-ref YOUR_PROJECT_REF
-
Check Edge Function has JWT_SECRET configured:
- Dashboard → Edge Functions → passkey → Settings
- Ensure
JWT_SECRETenvironment variable is set
-
Verify role assignment:
SELECT * FROM public.user_roles WHERE user_id = 'your-user-uuid';
For Both Methods:
-
Re-authenticate: Log out and log back in to get a fresh JWT with updated claims.
-
Check JWT payload in logs: Look for debug output showing parsed claims:
🔍 [RBAC DEBUG] JWT Claims parsed: user_role: user user_roles: [user, admin] is_admin: true
- Ensure the user making the call is an admin
- Check:
SELECT * FROM public.user_roles WHERE user_id = auth.uid() AND role = 'admin'; - First admin must be created manually via SQL
- Service role bypasses RLS - use for admin operations
- Check if user has required role:
SELECT public.user_has_role(auth.uid(), 'admin'); - Verify RLS policies allow the operation
- Ensure
java.util.Base64is available (should be standard in JVM) - Check JWT format (should be three dot-separated parts)
- Verify access token is not expired
- Role claims are cached in JWT (no database lookup on every request)
- JWT refresh updates role claims automatically
- RLS policies use efficient indexes on user_id and role columns
All tables have appropriate indexes:
CREATE INDEX idx_user_roles_user_id ON public.user_roles(user_id);
CREATE INDEX idx_user_roles_role ON public.user_roles(role);
CREATE INDEX idx_role_permissions_role ON public.role_permissions(role);- Use
user_has_role()function for simple checks - Use
authorize()in RLS policies (efficient with proper indexes) - Batch role checks when possible
- Role Hierarchy - Roles that inherit permissions from other roles
- Temporary Roles - Time-limited role assignments
- Role Groups - Collections of roles for easy assignment
- Audit Log UI - View who assigned what role and when
- Permission Builder - UI for creating custom permissions
- Multi-Tenant Roles - Roles scoped to specific workspaces/organizations
- Supabase RBAC Documentation
- PostgreSQL Row Level Security
- JWT Claims
- Issue #54 - Original requirement
For questions or issues with the RBAC system:
- Check this documentation
- Review the code comments in migration files
- Open an issue on GitHub
- Contact the development team
Version: 2.0.0 Last Updated: 2025-10-20 Author: BOSS Development Team
Changelog:
- v2.0.0 (2025-10-20): BREAKING CHANGE - Removed ENUM-based roles/permissions, migrated to fully table-based dynamic RBAC system
- v1.2.0 (2025-10-18): Added critical security model documentation (JWT signature verification)
- v1.1.0 (2025-10-18): Added passkey authentication RBAC integration documentation
- v1.0.0 (2025-01-18): Initial RBAC system release