Skip to content
← Back to blog
NestJSRBACSecurityAuthorization

NestJS RBAC Implementation Guide: Roles and Permissions

Firas Sayah·May 15, 2026·5 min read

Beyond Simple Roles

SaaS Starter ships with RBAC built in. See the authentication feature for the full auth and authorization system.

Most SaaS applications start with basic roles like "admin" and "user." That works for a while, but eventually customers need granular permissions: "This user can view invoices but not create them." A proper RBAC system gives you that flexibility without hard-coding every permission check.

Designing the Permission Model

Use a three-layer model: Users belong to Roles, and Roles contain Permissions.

@Entity()
export class Role {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  name: string;

  @ManyToMany(() => Permission, { eager: true })
  @JoinTable()
  permissions: Permission[];
}

@Entity()
export class Permission {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  key: string; // e.g., 'invoices:read', 'invoices:create'

  @Column()
  description: string;
}

Use a resource:action naming convention for permission keys. This makes them predictable and easy to query.

The Permissions Guard

Create a reusable guard that checks permissions on every request:

@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
      'permissions',
      [context.getHandler(), context.getClass()],
    );

    if (!requiredPermissions) return true;

    const { user } = context.switchToHttp().getRequest();
    return requiredPermissions.every((perm) =>
      user.permissions.includes(perm),
    );
  }
}

Custom Decorators

Cloudrix SaaS Starter ships with this pre-configured and tested.

Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.

Try the live demo →

Make permission checks declarative with a decorator:

export const RequirePermissions = (...permissions: string[]) =>
  SetMetadata('permissions', permissions);

Now protect any endpoint cleanly:

@Controller('invoices')
@UseGuards(JwtAuthGuard, PermissionsGuard)
export class InvoicesController {
  @Get()
  @RequirePermissions('invoices:read')
  findAll() {
    return this.invoicesService.findAll();
  }

  @Post()
  @RequirePermissions('invoices:create')
  create(@Body() dto: CreateInvoiceDto) {
    return this.invoicesService.create(dto);
  }

  @Delete(':id')
  @RequirePermissions('invoices:delete')
  remove(@Param('id') id: string) {
    return this.invoicesService.remove(id);
  }
}

Caching Permissions

Fetching permissions from the database on every request is expensive. Cache them in Redis with a short TTL:

@Injectable()
export class PermissionCacheService {
  constructor(private redis: RedisService) {}

  async getUserPermissions(userId: string): Promise<string[]> {
    const cached = await this.redis.get(`perms:${userId}`);
    if (cached) return JSON.parse(cached);

    const permissions = await this.loadFromDatabase(userId);
    await this.redis.set(`perms:${userId}`, JSON.stringify(permissions), 'EX', 300);
    return permissions;
  }

  async invalidate(userId: string) {
    await this.redis.del(`perms:${userId}`);
  }
}

Invalidate the cache whenever a user's role or permissions change.

Seeding Default Roles

Every SaaS needs sensible defaults. Seed them during application bootstrap:

async seedRoles() {
  const roles = [
    { name: 'owner', permissions: ['*'] },
    { name: 'admin', permissions: ['users:*', 'invoices:*', 'settings:read'] },
    { name: 'member', permissions: ['invoices:read', 'projects:read', 'projects:write'] },
    { name: 'viewer', permissions: ['invoices:read', 'projects:read'] },
  ];
  // Upsert roles and attach permissions
}

The wildcard * pattern lets you check permissions with a simple match function that supports both exact and wildcard comparisons.

Frontend Integration

Send the user's permissions in the JWT or fetch them on login. In Angular, use a directive or guard to hide UI elements the user cannot access. This prevents confusion: users should never see buttons they cannot click.

Testing RBAC

Write integration tests that verify each role can only access what it should:

it('should deny member from deleting invoices', async () => {
  const res = await request(app.getHttpServer())
    .delete('/invoices/123')
    .set('Authorization', `Bearer ${memberToken}`);
  expect(res.status).toBe(403);
});

Get Started Today

All of these patterns are fully implemented in SaaS Starter. Skip 2-6 weeks of setup.

F

Firas Sayah

Senior Software Engineer

Full-stack developer with 5+ years building production SaaS applications with NestJS and Angular.