Aviary

Authz

Laravel-style Gates & Policies for NestJS — a zero-dependency authorization core.

@dudousxd/nestjs-authz brings Laravel's Gate & Policy ergonomics to NestJS. You declare abilitiesview, update, delete, access-admin — as plain TypeScript functions and methods, then check them anywhere: in a route guard, in a service, or from a controller. The decision logic lives in code you can read, test, and step through with a debugger, not in an external policy DSL or a rules table.

Alpha

This library is in alpha (0.0.0). The API documented here matches the current source, but it may still change before a stable release. Pin an exact version in production.

Quickstart

The whole loop — install, register the module, write a policy, guard a route — in four steps. For the full walkthrough, see Getting Started.

Install the core package:

pnpm add @dudousxd/nestjs-authz

Register the module globally and point it at your policies:

app.module.ts
import { Module } from '@nestjs/common';
import { AuthzModule } from '@dudousxd/nestjs-authz';
import { PostPolicy } from './post.policy';

@Module({
  imports: [AuthzModule.forRoot({ policies: [PostPolicy] })],
})
export class AppModule {}

Write a policy — every method is an ability:

post.policy.ts
import { Policy } from '@dudousxd/nestjs-authz';
import { Post } from './post.entity';

@Policy(Post)
export class PostPolicy {
  update(user: User, post: Post) {
    return post.authorId === user.id;
  }
}

Guard the route with @Can — done:

posts.controller.ts
@Patch(':id')
@Can('update', Post) // loads the Post by :id, runs PostPolicy.update(currentUser, post)
update(@Param('id') id: string) {}

A denied check throws ForbiddenException (HTTP 403). The current user is read from nestjs-context automatically — or pass one explicitly with gate.forUser(user).

Authorization, not authentication

The single most important thing to internalize: authz answers "what may this user do?", never "who is this user?"

  • Authentication (authn) establishes identity. It runs login forms, validates passwords, issues and verifies JWTs or session cookies, and ultimately attaches a user to the request. That is the job of Passport, your JWT guard, your session middleware — whatever you already use.
  • Authorization (authz) takes an already-identified user and decides whether they're allowed to perform a specific action on a specific resource.

@dudousxd/nestjs-authz does the second job and only the second job. It never creates a user, never logs anyone in, never stores a password, and never touches your session store. It reads the current user (see Current user) and renders a verdict.

The name is a convention: authz = authorization, as opposed to authn = authentication. Bring your own authentication layer; authz slots in behind it.

The Laravel Gate/Policy idiom

If you've written Laravel, this will feel familiar. The core insight is that an authorization rule is just a function:

update(user: User, post: Post) {
  return post.authorId === user.id;
}

That method is the rule. There's no condition-as-data, no policy file to compile, no external evaluator. authz is a thin dispatcher: given an ability name like 'update' and a resource like a Post, it finds the matching function, calls it with (user, resource), and returns the boolean. This is deliberately not a CASL/Casbin-style engine — those model "condition as data" and "policy as external rules", which is a different (and heavier) mental model. authz keeps the rule in plain code.

The three layers

authz gives you three escalating tools. Reach for the simplest one that fits.

  1. Gates — ad-hoc, model-less abilities. gate.define('access-admin', (user) => user.role === 'staff'). Perfect for app-wide checks that aren't tied to a particular entity.
  2. Policies — a class per resource, the heart of the library. PostPolicy groups every ability for a Post (view, update, create, …) with an optional before bypass hook. This is where most of your authorization logic lives.
  3. RBAC — an opt-in, separate package for persisted roles and permissions. The core has zero database dependencies; RBAC ships as @dudousxd/nestjs-authz-{typeorm,mikro-orm,prisma} adapters that you wire in only if you need stored roles.

Zero-DB core, opt-in persistence

The core package is intentionally storage-agnostic. It has no database, no migrations, and no ORM dependency — exactly like Laravel's Gate/Policy sit apart from spatie/laravel-permission. Everything in Gates, Policies, and Enforcement runs purely in memory against the user object your auth layer produced.

When you do need persisted roles and permissions, you add an RBAC adapter. It plugs in through two optional seams (RoleProvider and PermissionProvider) without changing any of your policy or gate code. See RBAC.

Where the current user comes from

authz needs to know the current user to make a decision. It gets it two ways:

  • From @dudousxd/nestjs-context — when present, the gate reads the current user for free via the well-known CONTEXT_ACCESSOR token. nestjs-context is an optional peer dependency, consumed structurally; authz never imports it.
  • Explicitly — call gate.forUser(someUser) to check any user directly, with no context wiring required.

There's an important subtlety on the context path (you get a raw UserRef, not your hydrated entity). The Current user page covers it in full — read it before you ship.

Where to go next

On this page