Skip to content

RBAC

Workers Auth includes a full RBAC (Role-Based Access Control) system. Define roles with permissions, assign them to users, and gate routes by role or permission. Roles are persisted to D1 via the user_roles table, so they survive Worker restarts and work across replicas.

import { RBACPolicy } from 'workers-auth/authz/rbac';
const authz = RBACPolicy({
roles: {
admin: ['*'], // wildcard: all permissions
editor: ['read', 'write'],
user: ['read'],
},
defaultRole: 'user',
});

Pass the policy to WorkersAuth:

const auth = WorkersAuth({
// ...
authz,
});
OptionTypeDescription
rolesRecord<string, string[]>Map of role names to permission arrays
defaultRolestringRole assigned to every new user (must exist in roles)

The authorize middleware factory returns Hono middleware that checks whether the authenticated user meets a requirement.

// Only users with the "admin" role can access /admin/*
app.use('/admin/*', auth.authorize('role', 'admin'));
// Only users with the "write" permission can access POST /api/posts
app.post('/api/posts', auth.authorize('permission', 'write'), handler);

Important: auth.authenticate must run before auth.authorize. The authenticate middleware sets c.get('user'), which authorize reads.

app.use('/api/*', auth.authenticate);
app.use('/api/admin/*', auth.authorize('role', 'admin'));

A role with ['*'] as its permissions matches any permission check:

roles: {
admin: ['*'], // passes any auth.authorize('permission', '...') check
user: ['read'], // only passes auth.authorize('permission', 'read')
}

Wildcard applies to permission checks only. A role check (auth.authorize('role', 'admin')) always checks for the exact role name.

Roles are persisted to D1 via the user_roles table. You can manage them through the Admin API:

  • POST /users/:id/roles — assign a role
  • DELETE /users/:id/roles/:role — remove a role

The Admin API validates roles against your RBAC policy configuration, so you can only assign roles that are defined in your roles config.

The AuthorizationPolicy interface also exposes methods for runtime role management. The Admin API is the recommended approach for most use cases, but the programmatic interface is available when you need direct control:

interface AuthorizationPolicy {
getUserRoles(userId: string, db: DatabaseAdapter): Promise<string[]>;
getUserPermissions(userId: string, db: DatabaseAdapter): Promise<string[]>;
assignRole(userId: string, role: string, db: DatabaseAdapter): Promise<void>;
removeRole(userId: string, role: string, db: DatabaseAdapter): Promise<void>;
}
const auth = WorkersAuth({
database: (binding) => D1Adapter(binding),
cache: (binding) => KVAdapter(binding),
authn: {
strategies: [MagicLinkStrategy({ provider, template })],
},
authz: RBACPolicy({
roles: {
admin: ['*'],
editor: ['read', 'write'],
viewer: ['read'],
},
defaultRole: 'viewer',
}),
redirectUrl: '/dashboard',
});
app.route('/auth', auth.handler);
app.use('/api/*', auth.authenticate);
app.use('/api/admin/*', auth.authorize('role', 'admin'));
app.post('/api/posts', auth.authorize('permission', 'write'), createPost);
app.get('/api/posts', auth.authorize('permission', 'read'), listPosts);