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.1

Published

RBAC module for NestJS — hexagonal architecture

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 RbacAdapterOptions {
  roleRepository?: Type<RoleRepositoryPort> | RoleRepositoryPort;
  useControllers?: boolean;
  allowedPermissions?: string[];
}

| Option | Type | Default | Description | |--------|------|---------|-------------| | roleRepository | Class or instance implementing RoleRepositoryPort | InMemoryRoleRepository (dev/test only) | Persistence adapter for roles | | useControllers | boolean | off | Mounts the /roles routes. Off: no routes exist at all. | | allowedPermissions | string[] | none | The menu of permissions the /roles routes may grant. Required when useControllers is on (startup throws otherwise); [] means nothing is grantable. Each entry must match the permission format. No wildcards (*): startup throws, since a granted *:manage would pass rbac:manage. |

All three are wired statically at registration (also for registerAsync, where they sit next to useFactory, not inside it), so a class repository gets full DI. Returning one from the factory (the 0.1.x way) stops the app at startup instead of being ignored.

rbac:manage is not added to allowedPermissions for you; list it if admins should be able to grant it.

Async Registration

registerAsync takes the same options (adapter options stay next to useFactory):

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

@Module({
  imports: [
    RbacModule.registerAsync({
      useFactory: (config: ConfigService) => ({}),
      inject: [ConfigService],
      roleRepository: PrismaRoleRepository,
      useControllers: true,
      allowedPermissions: ['rbac:manage', 'order:read', 'order:write'],
    }),
  ],
})
export class AppModule {}

REST API

Mounted only when useControllers: true. Every route requires the rbac:manage permission (via PermissionsGuard, reading request.user.roles). Signed-out handling is your app's own auth guard; with no request.user the routes answer 403.

| Method | Path | Description | Status | |--------|------|-------------|--------| | POST | /roles | Create a role | 201 / 400 / 409 | | GET | /roles | List all roles | 200 | | GET | /roles/permissions | The allowedPermissions menu (string[]) | 200 | | GET | /roles/:name | Get a role by name | 200 / 404 | | PATCH | /roles/:name/permissions | Add or remove one permission; returns the updated role | 200 / 400 / 404 | | DELETE | /roles/:name | Delete a role | 204 / 404 |

Rules:

  • A permission outside allowedPermissions is 400 on create and on PATCH add (and the menu can't hold a wildcard, so *:* can't be granted through the API). PATCH remove is not menu-checked, so stale grants can still be cleaned up.
  • A new role name must be 1-64 lowercase letters, digits, _ or - (400 otherwise; route matching ignores case, so Admin and admin would share a URL). permissions is reserved (400): it would collide with GET /roles/permissions.
  • Nothing stops an admin deleting the admin role or removing rbac:manage from it. Restore it in your app's boot step.

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-z0-9_*]+:[a-z0-9_*]+$ |

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-z0-9_*]+:[a-z0-9_*]+$ |

Response 200 OK: the updated role. 400 if add names a permission outside allowedPermissions; 404 if the role does not exist.

GET /roles/permissions

Response 200 OK: the allowedPermissions array, e.g. ["order:read", "rbac:manage"].

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, digits, 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. Metadata is read from the handler, then the class. If neither sets permissions, 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 | | GetPermissionsForRolesPort | 'GetPermissionsForRolesPort' | GetPermissionsForRolesUseCase (deduped union; unknown roles ignored) | | RoleRepositoryPort | 'RoleRepositoryPort' | InMemoryRoleRepository (default) |

CheckPermissionPort, CheckAnyPermissionPort and GetPermissionsForRolesPort are exported from the module and can be injected directly into services, guards, or controllers in consuming apps. The most common ones to inject are CheckPermissionPort (single-role check) and CheckAnyPermissionPort (multi-role check, used by PermissionsGuard). RoleRepositoryPort is the only driven port — it is not exported because consumers interact with it indirectly through the driving ports.

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 })