npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@devoven/rbac

v0.1.0

Published

RBAC module for NestJS — hexagonal architecture

Downloads

121

Readme

@devoven/rbac

Role-based access control engine for NestJS. It answers one question: "does this role have this permission?"

Installation

npm install @devoven/rbac
# or
pnpm add @devoven/rbac

Peer dependencies

This package requires the standard NestJS peer dependencies. You likely already have most of these. The two that are not part of a vanilla NestJS install are class-validator and class-transformer, which are required for DTO validation:

npm install class-validator class-transformer

Quick Start

import { RbacModule } from '@devoven/rbac';

@Module({
  imports: [RbacModule.register({})],
})
export class AppModule {}

This uses InMemoryRoleRepository by default, which is suitable for development and testing.

Module Options

interface RbacModuleOptions {
  roleRepository?: Type<RoleRepositoryPort> | RoleRepositoryPort;
}

| Option | Type | Default | Description | |--------|------|---------|-------------| | roleRepository | Class or instance implementing RoleRepositoryPort | InMemoryRoleRepository | Persistence adapter for roles |

Pass a class and NestJS resolves its constructor dependencies. Pass an instance to provide a pre-configured object.

Async Registration

Use registerAsync when the repository depends on injected services or async configuration:

import { RbacModule } from '@devoven/rbac';
import { ConfigService } from '@nestjs/config';
import { PrismaRoleRepository } from './rbac/prisma-role.repository';

@Module({
  imports: [
    RbacModule.registerAsync({
      useFactory: (config: ConfigService) => ({
        roleRepository: config.get('USE_PRISMA')
          ? PrismaRoleRepository
          : undefined,
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

REST API

All endpoints are served under the /roles prefix.

| Method | Path | Description | Status | |--------|------|-------------|--------| | POST | /roles | Create a role | 201 / 409 | | GET | /roles | List all roles | 200 | | GET | /roles/:name | Get a role by name | 200 / 404 | | PATCH | /roles/:name/permissions | Add or remove a permission | 204 / 404 | | DELETE | /roles/:name | Delete a role | 204 / 404 |

POST /roles

Request body:

{
  "name": "editor",
  "permissions": ["article:read", "article:write"]
}

| Field | Type | Validation | |-------|------|------------| | name | string | Required | | permissions | string[] | At least 1 item, each matching ^[a-z_*]+:[a-z_*]+$ |

Response 201 Created:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "editor",
  "permissions": ["article:read", "article:write"],
  "createdAt": "2025-01-15T10:30:00.000Z",
  "updatedAt": "2025-01-15T10:30:00.000Z"
}

Returns 409 Conflict if a role with the same name already exists.

GET /roles

Response 200 OK: array of role objects (same shape as above).

GET /roles/:name

Response 200 OK: single role object. Returns 404 Not Found if the role does not exist.

PATCH /roles/:name/permissions

Request body:

{
  "action": "add",
  "permission": "article:delete"
}

| Field | Type | Validation | |-------|------|------------| | action | "add" or "remove" | Required | | permission | string | Must match ^[a-z_*]+:[a-z_*]+$ |

Response 204 No Content. Returns 404 Not Found if the role does not exist.

DELETE /roles/:name

Response 204 No Content. Returns 404 Not Found if the role does not exist.

Permission Format

Permissions use the resource:action pattern with lowercase letters, underscores, and wildcards:

order:read        exact match
order:*           matches any action on "order"
*:read            matches "read" on any resource
*:*               matches everything

Wildcard matching is handled by the Permission value object in the domain layer. A * in either segment matches any value in the corresponding position.

Guards and Decorators

Protect any route by combining @RequirePermissions with PermissionsGuard:

import { RequirePermissions, PermissionsGuard } from '@devoven/rbac';
import { UseGuards, Get } from '@nestjs/common';

@RequirePermissions('order:read')
@UseGuards(PermissionsGuard)
@Get('orders')
findAll() { ... }

The guard reads request.user.roles (a string[] of role names) and checks whether any of those roles has the required permission via CheckAnyPermissionPort. If request.user or request.user.roles is absent, access is denied. If the handler has no permissions set, the guard allows the request through.

Your authentication layer must populate request.user.roles before the guard runs.

Architecture

The module follows hexagonal architecture. The composition root (rbac.module.ts) is the only place that wires concrete adapters to port tokens.

Port / Token / Use Case Mapping

| Port Interface | DI Token | Use Case | |----------------|----------|----------| | CheckPermissionPort | 'CheckPermissionPort' | CheckPermissionUseCase | | CheckAnyPermissionPort | 'CheckAnyPermissionPort' | CheckAnyPermissionUseCase | | CreateRolePort | 'CreateRolePort' | CreateRoleUseCase | | GetRolePort | 'GetRolePort' | GetRoleUseCase | | ListRolesPort | 'ListRolesPort' | ListRolesUseCase | | AddPermissionPort | 'AddPermissionPort' | AddPermissionUseCase | | RemovePermissionPort | 'RemovePermissionPort' | RemovePermissionUseCase | | DeleteRolePort | 'DeleteRolePort' | DeleteRoleUseCase | | RoleRepositoryPort | 'RoleRepositoryPort' | InMemoryRoleRepository (default) |

CheckPermissionPort and CheckAnyPermissionPort are exported from the module and can be injected directly into guards in consuming apps.

Custom Adapters

Implement RoleRepositoryPort to use any backing store:

import { Injectable } from '@nestjs/common';
import { RoleRepositoryPort, Role, Permission } from '@devoven/rbac';

@Injectable()
export class PrismaRoleRepository implements RoleRepositoryPort {
  constructor(private readonly prisma: PrismaService) {}

  async save(role: Role): Promise<void> { /* ... */ }
  async findByName(name: string): Promise<Role | null> { /* ... */ }
  async findByNames(names: string[]): Promise<Role[]> { /* ... */ }
  async findAll(): Promise<Role[]> { /* ... */ }
  async delete(name: string): Promise<void> { /* ... */ }
}

When reading from the database, use Role.reconstitute(id, name, permissions, createdAt, updatedAt) to rebuild domain entities, and Permission.fromString('resource:action') to parse stored permission strings.

Then register it with the module:

RbacModule.register({ roleRepository: PrismaRoleRepository })