RBAC — Role-Based Access Control
ZewstID RBAC lets you define roles and permissions per application, assign them to users, and check them in your code. Roles are dual-written to Keycloak so they appear in JWT
resource_accessFor fine-grained, resource-level access (e.g. "can user X edit document:123?"), use Fine-Grained Authorization (FGA), which is built on the same tuple store as RBAC.
Quick Start
1. Create roles in the Developer Portal
Go to Applications → Your App → Roles & Permissions and create roles:
- — Full access
admin - — Can create and modify content
editor - — Read-only access
viewer
2. Create permissions
Permissions are
resource:action- — Can view reports
reports:read - — Can create and edit reports
reports:write - — Can manage users
users:manage
3. Attach permissions to roles
In the portal, attach permissions to each role:
- →
admin,reports:read,reports:writeusers:manage - →
editor,reports:readreports:write - →
viewerreports:read
4. Assign roles to users
Use the portal UI or the M2M API:
curl -X POST https://api.zewstid.com/api/v1/m2m/roles/assign \ -H "Authorization: Bearer $M2M_TOKEN" \ -H "Content-Type: application/json" \ -d '{"appId":"your-app-id","userId":"user-123","roleName":"editor"}'
5. Check in your code
Client-side (React) — JWT-based, instant:
import { useRBAC } from '@zewstid/id-nextjs'; function Dashboard() { const { hasRole, canAccess, roles } = useRBAC(); return ( <div> {hasRole('admin') && <AdminPanel />} {canAccess('reports', 'read') && <ReportsView />} </div> ); }
useRBAC()Server-side (Next.js API route):
import { createServerAuthz } from '@zewstid/id-nextjs/server'; const authz = createServerAuthz(); export async function GET(req: Request) { const session = await getServerSession(authOptions); const allowed = await authz.check({ user: session.user.id, relation: 'member', object: 'role:admin', }); if (!allowed) return new Response('Forbidden', { status: 403 }); // ... }
How It Works
- When you create a role in the Developer Portal, ZewstID stores a tuple in the authorization store and dual-writes a Keycloak client role.
- When you assign a role to a user, the role appears in their next JWT under .
resource_access.<your-app>.roles - The SDK's hook parses these claims — no network call needed.
useRBAC() - For checks that aren't expressible as a simple role (e.g., "is user X a member of team Y?"), use or
useAuthz()to query the runtime FGA engine.ServerAuthz
Tuples Under the Hood
Roles and permissions are stored as Zanzibar-style tuples:
(user:alice) --[member]--> (role:editor) (role:editor) --[parent]--> (permission:reports.write)
You don't normally interact with tuples for plain RBAC — the portal handles that. But it means RBAC and FGA share one query path, one cache, one audit log.
API Reference
App-level (bearer token from your app)
| Endpoint | Description |
|---|---|
GET /authz/apps/:appId/roles | List app roles |
POST /authz/apps/:appId/roles | Create role |
DELETE /authz/apps/:appId/roles/:roleName | Delete role |
GET /authz/apps/:appId/permissions | List permissions |
POST /authz/apps/:appId/permissions | Create permission |
POST /authz/apps/:appId/roles/:roleName/permissions | Attach permission to role |
POST /authz/apps/:appId/roles/:roleName/users | Assign role to user |
DELETE /authz/apps/:appId/roles/:roleName/users/:userId | Unassign role |
GET /authz/apps/:appId/users/:userId/roles | List user's roles |
POST /authz/check | Authorization check |
M2M (service account token)
| Endpoint | Required Scope | Description |
|---|---|---|
POST /m2m/roles/assign | roles:manage | Assign role to user |
DELETE /m2m/roles/remove | roles:manage | Remove role from user |
GET /m2m/users/:userId/roles | roles:read | List user's roles |
POST /m2m/authz/check | authz:check | Authorization check |
See the M2M Authentication guide for service account setup.
Authorization Check
curl -X POST https://api.zewstid.com/api/v1/authz/check \ -H "Authorization: Bearer $USER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"relation":"member","object":"role:admin"}'
Response:
{"allowed":true,"cached":false,"resolution_time_ms":3}The check API uses 3-layer caching: in-memory LRU (10s) → Redis tuple cache (60s) → Redis role cache (300s). Most checks resolve in single-digit milliseconds.
Best Practices
- Use for navigation and rendering decisions — it's instant (JWT-only) and zero network cost.
useRBAC() - Use /
useAuthz()for resource-level decisions — e.g., "can this user edit this specific document?"ServerAuthz - Don't trust client-side checks alone. Always re-verify on the server before mutating state.
- Keep role names short and resource-action permission names consistent (, not
reports:read).view-reports - One role per "job" — don't create per-user roles. Use FGA tuples for fine-grained per-resource access.
Next Steps
- Fine-Grained Authorization (FGA) — Resource-level access with tuples
- M2M Authentication — Service accounts and scopes
- SDK Reference — All hooks and utilities
Was this page helpful?
Let us know how we can improve our documentation