Skip to main content

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_access
claims automatically — no extra API calls for basic role checks.

For 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:

  • admin
    — Full access
  • editor
    — Can create and modify content
  • viewer
    — Read-only access

2. Create permissions

Permissions are

resource:action
pairs:

  • reports:read
    — Can view reports
  • reports:write
    — Can create and edit reports
  • users:manage
    — Can manage users

3. Attach permissions to roles

In the portal, attach permissions to each role:

  • admin
    reports:read
    ,
    reports:write
    ,
    users:manage
  • editor
    reports:read
    ,
    reports:write
  • viewer
    reports: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()
reads roles from the JWT — no network round-trip. Roles update on the next token refresh.

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

  1. When you create a role in the Developer Portal, ZewstID stores a tuple in the authorization store and dual-writes a Keycloak client role.
  2. When you assign a role to a user, the role appears in their next JWT under
    resource_access.<your-app>.roles
    .
  3. The SDK's
    useRBAC()
    hook parses these claims — no network call needed.
  4. For checks that aren't expressible as a simple role (e.g., "is user X a member of team Y?"), use
    useAuthz()
    or
    ServerAuthz
    to query the runtime FGA engine.

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)

EndpointDescription
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)

EndpointRequired ScopeDescription
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
    useRBAC()
    for navigation and rendering decisions
    — it's instant (JWT-only) and zero network cost.
  • Use
    useAuthz()
    /
    ServerAuthz
    for resource-level decisions
    — e.g., "can this user edit this specific document?"
  • 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 (
    reports:read
    , not
    view-reports
    ).
  • One role per "job" — don't create per-user roles. Use FGA tuples for fine-grained per-resource access.

Next Steps

Was this page helpful?

Let us know how we can improve our documentation