Roles & permissions (RBAC) for NestJS applications with TypeORM support.
This package provides a role-based access control (RBAC) system for NestJS that lets you manage roles and permissions with TypeORM, protect routes with guards, and check permissions programmatically.
Once installed, using it is as simple as:
@UseGuards(JwtAuthGuard, RolesGuard)
@RequireRoles('admin')
@Get('admin/dashboard')
dashboard() { return { message: 'Welcome, admin!' }; }- Installation
- Quick Start
- Module Configuration
- Using the Guards & Decorators
- Using the Service Directly
- Implementing PermissionUserRepository
- Configuration Options
- Features
- Testing
- Changelog
- Contributing
- Security
- Credits
- License
Install the package via npm:
npm install @nestbolt/permissionsOr via yarn:
yarn add @nestbolt/permissionsOr via pnpm:
pnpm add @nestbolt/permissionsThis package requires the following peer dependencies, which you likely already have in a NestJS project:
@nestjs/common ^10.0.0 || ^11.0.0
@nestjs/core ^10.0.0 || ^11.0.0
@nestjs/typeorm ^10.0.0 || ^11.0.0
typeorm ^0.3.0
class-validator ^0.14.0
class-transformer ^0.5.0
reflect-metadata ^0.1.13 || ^0.2.0
Optional:
@nestjs/event-emitter ^2.0.0 || ^3.0.0 # For permission/role lifecycle events
Create a repository that bridges your User entity with the permission system:
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import {
PermissionUserRepository,
UserHasRolesEntity,
UserHasPermissionsEntity,
} from "@nestbolt/permissions";
@Injectable()
export class TypeOrmPermissionUserRepository implements PermissionUserRepository {
constructor(
@InjectRepository(UserHasRolesEntity)
private readonly userRolesRepo: Repository<UserHasRolesEntity>,
@InjectRepository(UserHasPermissionsEntity)
private readonly userPermissionsRepo: Repository<UserHasPermissionsEntity>,
) {}
async getRoleIds(userId: string): Promise<string[]> {
const rows = await this.userRolesRepo.find({ where: { userId } });
return rows.map((r) => r.roleId);
}
async getDirectPermissionIds(userId: string): Promise<string[]> {
const rows = await this.userPermissionsRepo.find({ where: { userId } });
return rows.map((r) => r.permissionId);
}
async attachRoles(userId: string, roleIds: string[]): Promise<void> {
const entities = roleIds.map((roleId) =>
this.userRolesRepo.create({ userId, roleId }),
);
await this.userRolesRepo.save(entities);
}
async detachRoles(userId: string, roleIds: string[]): Promise<void> {
for (const roleId of roleIds) {
await this.userRolesRepo.delete({ userId, roleId });
}
}
async detachAllRoles(userId: string): Promise<void> {
await this.userRolesRepo.delete({ userId });
}
async attachPermissions(
userId: string,
permissionIds: string[],
): Promise<void> {
const entities = permissionIds.map((permissionId) =>
this.userPermissionsRepo.create({ userId, permissionId }),
);
await this.userPermissionsRepo.save(entities);
}
async detachPermissions(
userId: string,
permissionIds: string[],
): Promise<void> {
for (const permissionId of permissionIds) {
await this.userPermissionsRepo.delete({ userId, permissionId });
}
}
async detachAllPermissions(userId: string): Promise<void> {
await this.userPermissionsRepo.delete({ userId });
}
async userExists(userId: string): Promise<boolean> {
// Implement based on your User entity
return true;
}
}import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { PermissionsModule } from "@nestbolt/permissions";
import { TypeOrmPermissionUserRepository } from "./repositories/permission-user.repository";
@Module({
imports: [
TypeOrmModule.forRoot({
/* your config */
}),
PermissionsModule.forRoot({
userRepository: TypeOrmPermissionUserRepository,
}),
],
})
export class AppModule {}import { Controller, Get, UseGuards } from "@nestjs/common";
import { RequireRoles, RolesGuard } from "@nestbolt/permissions";
import { JwtAuthGuard } from "@nestbolt/authentication";
@Controller("admin")
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@RequireRoles("admin")
@Get("dashboard")
dashboard() {
return { message: "Welcome, admin!" };
}
}PermissionsModule.forRoot({
userRepository: TypeOrmPermissionUserRepository,
defaultGuardName: "default",
enableWildcardPermissions: false,
cache: { enabled: true, ttl: 86400000 },
displayPermissionInException: false,
displayRoleInException: false,
});PermissionsModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
userRepository: TypeOrmPermissionUserRepository,
enableWildcardPermissions: config.get("ENABLE_WILDCARD_PERMISSIONS", false),
}),
});The module is registered as global — you only need to import it once.
@UseGuards(JwtAuthGuard, PermissionsGuard)
@RequirePermissions('posts.create', 'posts.edit')
@Post('posts')
createPost() { /* user must have ALL listed permissions */ }@UseGuards(JwtAuthGuard, RolesGuard)
@RequireRoles('admin', 'editor')
@Get('manage')
manage() { /* user must have ANY of the listed roles */ }@UseGuards(JwtAuthGuard, RolesOrPermissionsGuard)
@RequireRolesOrPermissions('admin', 'posts.create')
@Post('posts')
createPost() { /* user must have the role 'admin' OR the permission 'posts.create' */ }Important: Always put
JwtAuthGuard(or your auth guard) before the permission/role guard sorequest.useris populated.
Inject PermissionRegistrarService for programmatic access:
import {
PermissionRegistrarService,
PermissionService,
RoleService,
} from "@nestbolt/permissions";
@Injectable()
export class SetupService {
constructor(
private readonly registrar: PermissionRegistrarService,
private readonly permissionService: PermissionService,
private readonly roleService: RoleService,
) {}
async seedRolesAndPermissions() {
// Create permissions
await this.permissionService.create("posts.create");
await this.permissionService.create("posts.edit");
await this.permissionService.create("posts.delete");
// Create roles and assign permissions
await this.roleService.create("admin");
await this.roleService.givePermissionTo(
"admin",
"posts.create",
"posts.edit",
"posts.delete",
);
await this.roleService.create("editor");
await this.roleService.givePermissionTo(
"editor",
"posts.create",
"posts.edit",
);
// Assign roles to users
await this.registrar.assignRole("user-uuid", "admin");
// Check permissions
await this.registrar.userHasPermissionTo("user-uuid", "posts.create"); // true
await this.registrar.userHasRole("user-uuid", "admin"); // true
}
}| Method | Returns | Description |
|---|---|---|
assignRole(userId, ...roles) |
Promise<void> |
Assign roles to a user |
removeRole(userId, ...roles) |
Promise<void> |
Remove roles from a user |
syncRoles(userId, ...roles) |
Promise<void> |
Replace all user roles with given ones |
givePermissionTo(userId, ...perms) |
Promise<void> |
Give direct permissions to a user |
revokePermissionTo(userId, ...perms) |
Promise<void> |
Revoke direct permissions from a user |
syncPermissions(userId, ...perms) |
Promise<void> |
Replace all direct user permissions |
userHasRole(userId, role) |
Promise<boolean> |
Check if user has a specific role |
userHasAnyRole(userId, roles) |
Promise<boolean> |
Check if user has any of the roles |
userHasAllRoles(userId, roles) |
Promise<boolean> |
Check if user has all roles |
userHasPermissionTo(userId, perm) |
Promise<boolean> |
Check permission (direct + via role + wildcard) |
userHasAnyPermission(userId, perms) |
Promise<boolean> |
Check if user has any permission |
userHasAllPermissions(userId, perms) |
Promise<boolean> |
Check if user has all permissions |
userHasDirectPermission(userId, perm) |
Promise<boolean> |
Check only direct permission |
getUserRoleNames(userId) |
Promise<string[]> |
Get all role names for a user |
getUserAllPermissions(userId) |
Promise<PermissionEntity[]> |
Get all permissions (direct + via roles) |
getUserPermissionsViaRoles(userId) |
Promise<PermissionEntity[]> |
Get permissions inherited from roles |
getUserDirectPermissions(userId) |
Promise<PermissionEntity[]> |
Get directly assigned permissions |
flushCache() |
void |
Clear the permission cache |
| Method | Returns | Description |
|---|---|---|
create(name, guardName?) |
Promise<PermissionEntity> |
Create a permission |
findByName(name, guardName?) |
Promise<PermissionEntity> |
Find by name (throws if not found) |
findById(id, guardName?) |
Promise<PermissionEntity> |
Find by ID (throws if not found) |
findOrCreate(name, guardName?) |
Promise<PermissionEntity> |
Find or create a permission |
findAll(guardName?) |
Promise<PermissionEntity[]> |
List all permissions |
findByNames(names, guardName?) |
Promise<PermissionEntity[]> |
Find multiple by names |
delete(nameOrId, guardName?) |
Promise<void> |
Delete a permission |
| Method | Returns | Description |
|---|---|---|
create(name, guardName?) |
Promise<RoleEntity> |
Create a role |
findByName(name, guardName?) |
Promise<RoleEntity> |
Find by name (throws if not found) |
findById(id, guardName?) |
Promise<RoleEntity> |
Find by ID (throws if not found) |
findOrCreate(name, guardName?) |
Promise<RoleEntity> |
Find or create a role |
findAll(guardName?) |
Promise<RoleEntity[]> |
List all roles |
delete(nameOrId, guardName?) |
Promise<void> |
Delete a role |
givePermissionTo(role, ...perms) |
Promise<RoleEntity> |
Attach permissions to a role |
revokePermissionTo(role, ...perms) |
Promise<RoleEntity> |
Detach permissions from a role |
syncPermissions(role, ...perms) |
Promise<RoleEntity> |
Replace all role permissions |
hasPermissionTo(role, perm) |
Promise<boolean> |
Check if role has a permission |
The PermissionUserRepository interface is the bridge between this package and your User entity. You must implement it and pass it to the module configuration.
| Method | Description |
|---|---|
getDirectPermissionIds(userId) |
Return IDs of directly assigned permissions |
getRoleIds(userId) |
Return IDs of assigned roles |
attachRoles(userId, roleIds) |
Insert user-role associations |
detachRoles(userId, roleIds) |
Remove user-role associations |
detachAllRoles(userId) |
Remove all roles from user |
attachPermissions(userId, permissionIds) |
Insert user-permission associations |
detachPermissions(userId, permissionIds) |
Remove user-permission associations |
detachAllPermissions(userId) |
Remove all direct permissions from user |
userExists(userId) |
Check if user exists |
See Quick Start for a full TypeORM implementation example.
| Option | Type | Default | Description |
|---|---|---|---|
userRepository |
Type<PermissionUserRepository> |
(required) | Class implementing PermissionUserRepository |
defaultGuardName |
string |
"default" |
Default guard name for roles/permissions |
enableWildcardPermissions |
boolean |
false |
Enable wildcard permission matching |
cache.enabled |
boolean |
true |
Enable in-memory permission cache |
cache.ttl |
number |
86400000 |
Cache TTL in milliseconds (default: 24 hours) |
displayPermissionInException |
boolean |
false |
Include permission names in 403 error messages |
displayRoleInException |
boolean |
false |
Include role names in 403 error messages |
Enable wildcard matching to define broad permissions:
PermissionsModule.forRoot({
userRepository: TypeOrmPermissionUserRepository,
enableWildcardPermissions: true,
});// Give a user the wildcard permission
await permissionService.create("articles.*");
await registrar.givePermissionTo(userId, "articles.*");
// Now these checks all return true:
await registrar.userHasPermissionTo(userId, "articles.create"); // true
await registrar.userHasPermissionTo(userId, "articles.edit"); // true
await registrar.userHasPermissionTo(userId, "articles.delete"); // true
// Super-admin wildcard
await permissionService.create("*");
await registrar.givePermissionTo(userId, "*");
await registrar.userHasPermissionTo(userId, "anything.at.all"); // trueIf @nestjs/event-emitter is installed, the package emits events on role/permission changes:
| Event | Payload | When |
|---|---|---|
permissions.role-attached |
{ userId, roleIds } |
Role(s) assigned to user |
permissions.role-detached |
{ userId, roleIds } |
Role(s) removed from user |
permissions.permission-attached |
{ userId, permissionIds } |
Permission(s) given to user |
permissions.permission-detached |
{ userId, permissionIds } |
Permission(s) revoked from user |
permissions.role-created |
{ role } |
New role created |
permissions.role-deleted |
{ role } |
Role deleted |
permissions.permission-created |
{ permission } |
New permission created |
permissions.permission-deleted |
{ permission } |
Permission deleted |
permissions.cache-flushed |
{} |
Permission cache cleared |
The in-memory cache stores permission data to avoid repeated database queries. It is automatically flushed when roles or permissions are modified. You can also flush it manually:
registrar.flushCache();npm testRun tests in watch mode:
npm run test:watchGenerate coverage report:
npm run test:covPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.
The MIT License (MIT). Please see License File for more information.