Skip to content

Roles & Permissions

Roles & Permissions

The admin system uses a role-based access control (RBAC) model with granular permissions. Roles are stored in the ADMIN_DB D1 database and cached at the edge via KV.

Built-in Roles

Three roles are seeded by the migration and cover the most common access patterns:

RoleDisplay NameDescriptionPermissions
viewerViewerRead-only access to admin dashboards and logs6 permissions
editorEditorRead and write access to configuration, flags, and tiers16 permissions
super-adminSuper AdminFull administrative access including user management and role assignment27 permissions (all)

Super-admin bypass: Users with the super-admin role skip all permission checks — similar to Unix root. This is enforced in the middleware before any permission evaluation occurs.

Viewer Permissions

admin:read, audit:read, metrics:read, config:read, users:read, flags:read

Editor Permissions

admin:read, audit:read, metrics:read, config:read, config:write,
users:read, flags:read, flags:write, tiers:read, tiers:write,
scopes:read, scopes:write, endpoints:read, endpoints:write,
announcements:read, announcements:write

Super-Admin Permissions

All 27 permissions (see full list below). Super-admins also bypass the permission check entirely.

All 27 Permissions

Defined in AdminPermissionSchema (worker/schemas.ts):

PermissionCategoryDescription
admin:readAdminView admin dashboard and system state
admin:writeAdminModify admin system configuration
audit:readAuditQuery audit log entries
metrics:readMetricsView system metrics and analytics
config:readConfigRead tier, scope, and endpoint configuration
config:writeConfigModify tier, scope, and endpoint configuration
users:readUsersView user list and user details
users:writeUsersUpdate user records
users:manageUsersSuspend, activate, and manage user lifecycle
flags:readFlagsView feature flags
flags:writeFlagsCreate, update, and delete feature flags
tiers:readTiersView tier configuration
tiers:writeTiersCreate, update, and delete tier configuration
scopes:readScopesView scope configuration
scopes:writeScopesCreate, update, and delete scope configuration
endpoints:readEndpointsView endpoint auth overrides
endpoints:writeEndpointsCreate, update, and delete endpoint auth overrides
announcements:readAnnouncementsView system announcements
announcements:writeAnnouncementsCreate, update, and delete announcements
roles:readRolesView role definitions
roles:writeRolesCreate and modify role definitions
roles:assignRolesAssign and revoke roles for users
keys:readAPI KeysView API key metadata
keys:writeAPI KeysCreate API keys
keys:revokeAPI KeysRevoke API keys
storage:readStorageView storage statistics and data
storage:writeStorageClear cache, run vacuum, and manage storage

Role Resolution Flow

Every admin API request resolves the caller’s role and permissions before evaluating access. The flow prioritizes speed via KV caching:

flowchart TD
    A["Incoming Request<br/>with Clerk JWT"] --> B{"JWT valid?<br/>verifyClerkJWT()"}
    B -->|No| C["401 Unauthorized"]
    B -->|Yes| D{"publicMetadata.role<br/>=== 'admin'?"}
    D -->|No| E["403 Forbidden<br/>(not an admin)"]
    D -->|Yes| F{"KV cache hit?<br/>admin:role:{userId}"}
    F -->|Hit| G["Use cached<br/>role + permissions"]
    F -->|Miss| H["D1 query<br/>JOIN admin_role_assignments<br/>→ admin_roles"]
    H --> I{"Assignment found<br/>and not expired?"}
    I -->|No| J["403 Forbidden<br/>(no active role)"]
    I -->|Yes| K["Write-through<br/>to KV cache<br/>TTL 300s"]
    K --> G
    G --> L{"Has required<br/>permission?"}
    L -->|No| M["403 Forbidden<br/>(insufficient permission)"]
    L -->|Yes| N["✅ Request proceeds"]

Cache Details

  • Key format: admin:role:{clerkUserId}
  • Namespace: RATE_LIMIT (shared KV namespace, uses admin: prefix)
  • TTL: 300 seconds (5 minutes)
  • Invalidation: Explicit delete on role assign/revoke via invalidateRoleCache()
  • Stored value: JSON { role_name, permissions[], expires_at }

Middleware Pipeline

Admin routes are protected by middleware functions defined in worker/middleware/admin-role-middleware.ts. The pipeline runs in order:

flowchart LR
    A["requireAdminPermission(<br/>'config:write')"] --> B["verifyClerkJWT()"]
    B --> C["checkAdminRole()<br/>publicMetadata gate"]
    C --> D["resolveAdminContext()<br/>D1/KV role lookup"]
    D --> E["checkPermission()<br/>super-admin bypass<br/>or permission match"]
    E --> F["Handler executes"]

Guard Functions

Three middleware variants are exported for different use cases:

// Require a single permission
requireAdminPermission('config:write')
// Require at least one of several permissions (OR logic)
requireAnyAdminPermission(['config:write', 'tiers:write'])
// Require all listed permissions (AND logic)
requireAllAdminPermissions(['audit:read', 'metrics:read'])

There is also extractAdminContext() which resolves the caller’s role and permissions without enforcing a specific permission check. This is used by the “my context” endpoint.

AdminGuardResult

Every guard returns a discriminated union:

type AdminGuardResult =
| { authorized: true; adminContext: ResolvedAdminContext }
| { authorized: false; error: string; status: 401 | 403 };

Handlers check .authorized and return the appropriate HTTP status if access is denied.

Role Management API

Roles are managed via the admin API. Only users with the roles:write or roles:assign permission can modify roles.

OperationEndpointPermission
List rolesGET /admin/system/rolesadmin:read
Create rolePOST /admin/system/rolesroles:write
Update rolePATCH /admin/system/roles/:idroles:write
List assignmentsGET /admin/system/roles/assignmentsadmin:read
Assign rolePOST /admin/system/roles/assignroles:assign
Revoke roleDELETE /admin/system/roles/revokeroles:assign

Example: Assign a role

Terminal window
curl -X POST https://your-worker.workers.dev/admin/system/roles/assign \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"clerk_user_id": "user_2abc123",
"role_name": "editor",
"expires_at": "2025-12-31T23:59:59Z"
}'

Example: Revoke a role

Terminal window
curl -X DELETE https://your-worker.workers.dev/admin/system/roles/revoke \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"clerk_user_id": "user_2abc123",
"role_name": "editor"
}'

Custom Roles

Beyond the three built-in roles, you can create custom roles with any combination of the 27 permissions:

Terminal window
curl -X POST https://your-worker.workers.dev/admin/system/roles \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{
"role_name": "flag-manager",
"display_name": "Flag Manager",
"description": "Can manage feature flags but nothing else",
"permissions": ["admin:read", "flags:read", "flags:write"]
}'

Custom roles follow the same resolution flow and caching behavior as built-in roles.