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

nestjs-permissions

v0.1.1

Published

Composable permission-loading and permission-checking guards for NestJS

Readme

nestjs-permissions

npm version CI license

Declarative, permission-based authorization for NestJS — two global guards that load a user's permissions and check them against per-route rules, driven entirely by decorators.

@Controller('apps')
@LoadPermissions('apps')                          // load the user's app permissions
export class AppsController {
  @Patch(':id')
  @CheckPermissions('apps.edit', 'apps.editableAppIds(:id)')  // both must pass
  update(@Param('id') id: string) { /* ... */ }
}

No @UseGuards() boilerplate, no permission logic scattered through controllers — just say what each route needs.


Contents

Why

This library is authorization only. It assumes something upstream (a JWT/Passport guard, a session middleware, …) has already authenticated the request and set request.user. From there it answers one question per route: is this user allowed to do this?

  • Declarative — permissions are expressed as decorator metadata, next to the route.
  • Two-phase — loading (fetch what the user can do) is separated from checking (enforce it), so you fetch permissions once and evaluate many rules against them.
  • Portable — the guards depend only on @nestjs/common/@nestjs/core. All app-specific logic (DB queries, caching) lives in your PermissionsLoaderService.
  • Type-safe (opt-in) — describe your permission shape once and get compile-time errors for invalid rule strings.

How it works

Once registered, both guards run on every request, in order:

  1. PermissionsLoaderGuard reads @LoadPermissions(...) metadata, calls your PermissionsLoaderService.loadPermissions(user, keys, ctx), and merges the result onto request.user.permissions.
  2. PermissionsCheckGuard reads @CheckPermissions(...) metadata and evaluates each rule against request.user.permissions (and route params), throwing ForbiddenException on the first failing rule.

A route with neither decorator is a no-op — the guards let it through. A route missing request.user when a check is required is rejected with 403 (the loader guard, by contrast, no-ops on a missing user and leaves rejection to your auth layer).

Requirements

  • NestJS 10 or 11 (@nestjs/common, @nestjs/core)
  • reflect-metadata and rxjs (already present in every Nest app)

These are declared as peer dependencies, so the library uses whatever versions your app has.

Installation

npm install nestjs-permissions

Quick start

1. Implement a loader — this is where "what can this user do?" is answered.

import { Injectable } from '@nestjs/common';
import { PermissionsLoaderService } from 'nestjs-permissions';

@Injectable()
export class AppPermissionsLoader extends PermissionsLoaderService {
  async loadPermissions(user: { id: number }, keys: string[]) {
    const result: Record<string, any> = {};
    if (keys.includes('apps')) {
      // ...look up from your DB/cache...
      result.apps = { edit: true, editableAppIds: [1, 2, 3] };
    }
    return result; // -> merged onto request.user.permissions
  }
}

2. Register the module once in AppModule — this wires up both global guards.

import { Module } from '@nestjs/common';
import { PermissionsModule } from 'nestjs-permissions';
import { AppPermissionsLoader } from './app-permissions-loader.service';

@Module({
  imports: [PermissionsModule.forRoot(AppPermissionsLoader)],
})
export class AppModule {}

If your loader needs injected dependencies (a Prisma service, a repository), use forRootAsync:

PermissionsModule.forRootAsync({
  imports: [PrismaModule],
  inject: [PrismaService],
  useFactory: (prisma: PrismaService) => new AppPermissionsLoader(prisma),
});

3. Declare load + check rules on your controllers.

import { Controller, Get, Patch, Param } from '@nestjs/common';
import { LoadPermissions, CheckPermissions } from 'nestjs-permissions';

@Controller('apps')
@LoadPermissions('apps') // loaded once for every route in this controller
export class AppsController {
  @Get()
  @CheckPermissions('apps.view')
  list() {}

  @Patch(':id')
  @CheckPermissions(
    'apps.edit',                 // must have the edit flag …
    'apps.editableAppIds(:id)',  // … AND :id must be in the editable list
  )
  update(@Param('id') id: string) {}
}

That's it — the guards enforce these on every request automatically.

Rules in depth

Rule syntax reference

| Rule | Meaning | |---|---| | 'apps.create' | dot-path lookup on user.permissions; passes only if strictly === true | | 'apps.editableAppIds(:id)' | the path must resolve to an array, and the route's :id param must be in it (compared with String(), so numeric and string ids both work) | | (user, request, context) => boolean | fully custom; may be async; throw to produce your own error instead of the generic ForbiddenException |

Multiple rules are AND-ed — @CheckPermissions('a', 'b') requires both. Passing a function alone is fine (no leading string required):

@CheckPermissions((user, request) =>
  user.role === 'owner' || request.body?.confirmed === true,
)

Class-level + method-level (merged)

@LoadPermissions and @CheckPermissions can be applied at the class level, the method level, or both. The two are merged and deduplicated, never overridden — a method's declarations are added to the class's.

@Controller('apps')
@LoadPermissions('apps')          // loaded for every method
@CheckPermissions('apps.access')  // required for every method
export class AppsController {
  @Get()
  list() {} // load: ['apps'] · check: ['apps.access']

  @Patch(':id')
  @LoadPermissions('billing')     // also loads 'billing'
  @CheckPermissions('apps.edit')  // also requires 'apps.edit'
  update() {} // load: ['apps', 'billing'] · check: ['apps.access', 'apps.edit']
}

Because checks are AND-ed across the merged set, a class rule is an additive constraint: a method can add requirements but can't loosen the class-level one.

Public routes

@Public() opts a route out of both guards entirely:

import { Public } from 'nestjs-permissions';

@Controller('apps')
@LoadPermissions('apps')
export class AppsController {
  @Public()
  @Get('health')
  health() {} // no user required, nothing loaded, nothing checked

  @Patch(':id')
  @CheckPermissions('apps.edit')
  update() {} // still enforced
}

It works at class or method level and is read with getAllAndOverride (method wins), matching the standard opt-out pattern used alongside a global auth guard.

⚠️ Sharp edge: because @Public() short-circuits both guards, a @CheckPermissions on a method inside a @Public() class will never run. Don't combine them on the same route.

Reading permissions in a handler

@UserPermissions() injects request.user.permissions (the object the loader guard populated) into a controller method:

import { UserPermissions } from 'nestjs-permissions';

@Get()
list(@UserPermissions() permissions: AppPermissions) {
  return permissions.apps;
}

Pass a dot-path to pluck a sub-key (resolved the same way @CheckPermissions string rules are):

| Argument | Returns | |---|---| | @UserPermissions() | user.permissions | | @UserPermissions('apps') | user.permissions.apps | | @UserPermissions('apps.edit') | user.permissions.apps.edit |

It returns undefined when there's no request.user (an unauthenticated or @Public() route) or when a path segment is missing — traversal never throws. A param decorator can't enforce the parameter's type, so annotate it yourself.

Type-safe rules (optional)

By default a rule string is just a string, so a typo like 'apps.edt' compiles fine and only fails at runtime. Describe your permission shape once with createPermissions and get compile-time errors for any path or bucket that doesn't exist:

// permissions.ts
import { createPermissions } from 'nestjs-permissions';

export interface AppPermissions {
  apps: { create: boolean; edit: boolean; editableAppIds: number[] };
  billing: { manage: boolean };
}

export const { CheckPermissions, LoadPermissions } =
  createPermissions<AppPermissions>();
// controller — import from your permissions.ts, not the package
import { CheckPermissions, LoadPermissions } from '../permissions';

@LoadPermissions('apps', 'billing')            // ok — top-level buckets
@LoadPermissions('reports')                    // ❌ compile error: not a bucket
@CheckPermissions('apps.edit')                 // ok
@CheckPermissions('apps.editableAppIds(:id)')  // ok — array leaf, param form
@CheckPermissions((user, req) => user.role === 'owner') // ok — functions still allowed
@CheckPermissions('apps.delete')               // ❌ compile error: not in AppPermissions
@CheckPermissions('apps.editableAppIds')       // ❌ compile error: array leaf needs (:param)

Schema → valid values:

  • @LoadPermissions accepts the top-level bucket keys ('apps', 'billing').
  • @CheckPermissions: a boolean leaf → dot-path rule; an array leaf → (:param) rule (any param name); nested objects are walked recursively.

This is purely compile-time — the typed decorators emit the exact same metadata as the package's CheckPermissions/LoadPermissions, so runtime behavior is unchanged. The untyped exports remain available for callers that don't want a schema.

API

| Export | Kind | Purpose | |---|---|---| | PermissionsModule | module | forRoot(loader) / forRootAsync(options) — registers both global guards | | PermissionsLoaderService | abstract class | extend + implement loadPermissions(user, keys, ctx) | | @LoadPermissions(...keys) | decorator | declare which permission buckets to load | | @CheckPermissions(...rules) | decorator | declare the rules a route requires (AND-ed) | | @Public() | decorator | opt a route out of both guards | | @UserPermissions(path?) | param decorator | inject request.user.permissions (or a sub-path) | | createPermissions<T>() | factory | returns typed CheckPermissions / LoadPermissions | | PermissionsLoaderGuard, PermissionsCheckGuard | guards | registered for you by the module | | PermissionRule, PermissionRuleFn, PermissionPaths, … | types | for advanced/typed usage |

Example app

A complete, runnable NestJS app lives in example/: a PermissionsLoaderService, typed decorators, every rule form (boolean / array-param / function), @Public(), @UserPermissions(), and an end-to-end test suite that drives real HTTP requests through the guards.

Development

npm install
npm run check   # build + type fixtures + unit tests + example e2e (the full gate)

| Script | What it does | |---|---| | npm run build | tsc compile to dist/ | | npm run test:types | type-checks the type-tests/ @ts-expect-error fixtures | | npm test | Jest unit suite (src/**/*.spec.ts) — guards, @UserPermissions(), module wiring | | npm run test:example | e2e suite in example/, against the built dist/ | | npm run check | all of the above, in order, stopping at the first failure |

Also: npm run test:watch, npm run test:cov.

Releases are published to npm automatically by GitHub Actions when a GitHub Release is created (see .github/workflows/npm-publish.yml), using npm provenance.

License

MIT © Midhun G S