Roles
Roles are the foundation of Zelt's authorization system. They define what a user can do.
What is a Role?
A role is a simple string that represents a permission level or capability:
['admin', 'editor', 'viewer']
['owner', 'member', 'guest']
['read:users', 'write:users', 'delete:users']
Roles are assigned during authentication via setUser():
setUser(
{ id: user.id, name: user.name },
['admin', 'user'] // ← roles
);
Defining Role Types
Use RequestContextSchema to type your roles:
declare module '@zeltjs/core' {
interface RequestContextSchema {
user: { id: string; name: string };
authRoles: ('admin' | 'editor' | 'viewer')[];
}
}
This provides:
- Autocomplete when calling
setUser() - Type checking in
@Authorized(['...']) - Type-safe
currentRoles()return value
Role Design Patterns
Hierarchical Roles
Define roles that imply other roles:
type Role = 'admin' | 'editor' | 'viewer';
const roleHierarchy: Record<Role, Role[]> = {
admin: ['admin', 'editor', 'viewer'],
editor: ['editor', 'viewer'],
viewer: ['viewer'],
};
// When setting user, expand roles
setUser(user, roleHierarchy[user.primaryRole]);
Resource-Scoped Roles
Include resource context in role names:
type Role =
| 'admin'
| `project:${string}:owner`
| `project:${string}:member`
| `team:${string}:admin`;
// User is owner of project-123, member of team-456
setUser(user, ['project:123:owner', 'team:456:admin']);
Permission-Based Roles
Use fine-grained permission strings:
type Permission =
| 'read:users'
| 'write:users'
| 'delete:users'
| 'read:posts'
| 'write:posts';
// Roles map to permissions
const rolePermissions: Record<string, Permission[]> = {
admin: ['read:users', 'write:users', 'delete:users', 'read:posts', 'write:posts'],
editor: ['read:users', 'read:posts', 'write:posts'],
viewer: ['read:users', 'read:posts'],
};
Where Roles Come From
Database
Store roles with the user record:
// User table
interface User {
id: string;
name: string;
roles: string[]; // ['admin', 'user']
}
// In authentication middleware
const user = await db.users.findById(payload.sub);
setUser(
{ id: user.id, name: user.name },
user.roles
);
JWT Claims
Include roles in the JWT payload:
// When signing
const token = await jwtService.sign({
sub: user.id,
roles: user.roles,
});
// When verifying (in JwtConfig.resolveUser)
override get resolveUser() {
return async (payload: JwtPayload) => ({
user: { id: payload.sub },
roles: payload.roles as string[],
});
}
Session Data
Store roles in the session:
// At login
setSession({ userId: user.id, roles: user.roles });
// In auth middleware
const session = getSession();
if (session) {
const user = await db.users.findById(session.userId);
setUser(user, session.roles);
}
External Service
Fetch roles from an identity provider:
const userInfo = await identityProvider.getUserInfo(token);
const roles = await identityProvider.getRoles(userInfo.sub);
setUser(
{ id: userInfo.sub, name: userInfo.name },
roles
);
Role Assignment Strategies
Static Assignment
Roles are set once and rarely change:
// Admin assigns roles via API
@Authorized(['admin'])
@Post('/users/:id/roles')
async assignRoles(id = pathParam('id'), body = bodyParam(RolesSchema)) {
await db.users.update(id, { roles: body.roles });
return { success: true };
}
Dynamic Assignment
Roles are computed based on context:
// Roles depend on resource ownership
const project = await db.projects.findById(projectId);
const roles = [];
if (project.ownerId === user.id) {
roles.push('project:owner');
}
if (project.memberIds.includes(user.id)) {
roles.push('project:member');
}
setUser(user, roles);
Time-Based Roles
Roles expire or activate based on time:
const roles = user.roles.filter(role => {
const grant = user.roleGrants.find(g => g.role === role);
if (!grant) return true;
const now = Date.now();
if (grant.startsAt && now < grant.startsAt) return false;
if (grant.expiresAt && now > grant.expiresAt) return false;
return true;
});
setUser(user, roles);
Accessing Roles
In Handlers
import { currentRoles } from '@zeltjs/core';
@Get('/dashboard')
dashboard() {
const roles = currentRoles();
return {
canManageUsers: roles.includes('admin'),
canEditContent: roles.includes('editor') || roles.includes('admin'),
};
}
In Services
class PostService {
canDelete(post: Post): boolean {
const roles = currentRoles();
const user = currentUser();
if (roles.includes('admin')) return true;
if (post.authorId === user?.id) return true;
return false;
}
}
Best Practices
Keep Roles Simple
Use flat strings, not nested objects:
// ✅ Good
['admin', 'editor', 'viewer']
// ❌ Avoid
[{ name: 'admin', level: 10, permissions: [...] }]
Use Roles for Coarse Access
Roles answer "can this user access this feature area?" not "can this user edit this specific record?":
// ✅ Role-based: "Can access admin section"
@Authorized(['admin'])
@Get('/admin/dashboard')
// ❌ Not a role: "Can edit post #123"
// → Handle in service logic instead
Avoid Role Explosion
Don't create roles for every action:
// ❌ Too many roles
['can_view_users', 'can_create_users', 'can_edit_users', 'can_delete_users', ...]
// ✅ Group into meaningful roles
['admin', 'user_manager', 'viewer']
Document Your Roles
Maintain a central reference:
/**
* Application Roles
*
* - admin: Full system access
* - editor: Can create and modify content
* - viewer: Read-only access
* - moderator: Can manage user-generated content
*/
type Role = 'admin' | 'editor' | 'viewer' | 'moderator';