NestJS Integration

The nest-warden/nestjs module is a first-party NestJS integration — not a third-party adapter. One forRoot() call wires up the full stack: tenant resolution, per-request ability building, route-level policy enforcement, and the @CurrentTenant parameter decorator.

Setup

1. Define your action and subject vocabulary

// src/auth/permissions.ts
import { type MongoAbility } from '@casl/ability';
import type { TenantAbilityBuilder, TenantContext } from 'nest-warden';

export type AppAction = 'read' | 'create' | 'update' | 'delete' | 'manage';
export type AppSubject = 'Merchant' | 'Payment' | 'Agent' | 'all';
export type AppAbility = MongoAbility<[AppAction, AppSubject]>;

export function defineAbilities(
  builder: TenantAbilityBuilder<AppAbility>,
  ctx: TenantContext,
): void {
  if (ctx.roles.includes('iso-admin')) {
    builder.can('manage', 'Merchant');
  }
  if (ctx.roles.includes('agent')) {
    builder.can('read', 'Merchant', { agentId: ctx.subjectId });
  }
}

2. Register the module

// src/app.module.ts
import { Module } from '@nestjs/common';
import { TenantAbilityModule } from 'nest-warden/nestjs';
import { defineAbilities, type AppAbility } from './auth/permissions';

@Module({
  imports: [
    TenantAbilityModule.forRoot<AppAbility>({
      // Field name on resources that carries the tenant FK. Default 'tenantId'.
      tenantField: 'tenantId',

      // Server-side resolution — see callout below.
      resolveTenantContext: async (req) => {
        const user = (req as { user: JwtPayload }).user;
        const m = await tenantMembershipsRepo.findOneBy({
          userId: user.sub,
          tenantId: user.claimedTenantId,
          status: 'ACTIVE',
        });
        if (!m) throw new ForbiddenException('No active tenant membership');
        return {
          tenantId: m.tenantId,
          subjectId: m.userId,
          roles: m.roles,
        };
      },

      // Define per-request rules.
      defineAbilities,

      // Optional: relationship graph for $relatedTo rules.
      graph: relationshipGraph,
    }),
    // ... other modules
  ],
})
export class AppModule {}

3. Use it in controllers

import { Controller, Get, Param, Inject } from '@nestjs/common';
import { CheckPolicies } from 'nest-warden/nestjs';
import type { AppAbility } from '../auth/permissions';

@Controller('merchants')
export class MerchantsController {
  constructor(
    @Inject(MerchantsService)
    private readonly merchants: MerchantsService,
  ) {}

  @CheckPolicies((ability: AppAbility) => ability.can('read', 'Merchant'))
  @Get()
  list() {
    return this.merchants.findAll();
  }

  @CheckPolicies((ability: AppAbility) => ability.can('read', 'Merchant'))
  @Get(':id')
  get(@Param('id') id: string) {
    return this.merchants.findOne(id);
  }
}

The TenantPoliciesGuard (registered as a global APP_GUARD by default) runs every @CheckPolicies handler and throws ForbiddenException on any false return.

Decorators

DecoratorPurpose
@Public()Skip auth + tenant context entirely. Use for health checks, public landing pages.
@CheckPolicies(...handlers)Attach one or more policy handlers to a route. Each handler is (ability) => boolean.
@AllowCrossTenant(reasonCode)Mark a route as deliberately cross-tenant. Used with crossTenant.can() rules and audited.
@CurrentTenant()Inject the resolved TenantContext into a controller param.
@CurrentTenant('tenantId')Inject a single field from the context.

Custom auth integration

The library doesn't ship its own auth — it expects request.user to be populated by your auth guard (Passport JWT, Auth0, custom JWT middleware, etc.) before TenantPoliciesGuard runs.

A typical setup uses two global guards:

// app.module.ts
@Module({
  imports: [TenantAbilityModule.forRoot({ ... })],
  providers: [
    // Your auth guard runs FIRST and populates request.user.
    { provide: APP_GUARD, useClass: JwtAuthGuard },
    // (TenantPoliciesGuard is auto-registered by TenantAbilityModule)
  ],
})
export class AppModule {}

The example app at examples/nestjs-app/ uses a FakeAuthGuard that reads request.user from a header — replace with @nestjs/passport's JWT guard for production.

Guard ordering

NestJS runs guards in registration order. The library's default arrangement:

  1. Your auth guard (sets request.user).
  2. TenantPoliciesGuard (lazy-resolves TenantContext, builds the per-request ability, runs @CheckPolicies handlers).

The policies guard lazy-resolves the tenant context — it doesn't depend on TenantContextInterceptor running first. NestJS runs guards before interceptors, so depending on an interceptor here would be a bug; we explicitly avoid that.

This means TenantContextInterceptor is optional. Register it manually if you have middleware-style consumers (request loggers, metric emitters) that need the context before the policies guard runs — for instance, a logger that tags every request with its tenant ID.

esbuild and decorators

NestJS's auto-DI uses TypeScript's emitDecoratorMetadata to discover constructor parameter types. Tools that compile via esbuild (tsup, tsx, Vitest's default transformer) do not implement this metadata emit. Class-typed constructor parameters resolve to undefined and guards / services crash at runtime.

The library uses explicit @Inject(<Token>) everywhere internally, so it works under all bundlers. Your app code should follow the same pattern:

// ❌ Breaks under esbuild-based tools
constructor(private readonly merchants: MerchantsService) {}

// ✅ Bundler-agnostic
constructor(
  @Inject(MerchantsService)
  private readonly merchants: MerchantsService,
) {}

Module options reference

interface TenantAbilityModuleOptions<TAbility, TId extends TenantIdValue = string> {
  /** Resource field carrying the tenant ID. Default: 'tenantId'. */
  tenantField?: string;

  /** CASL ability factory or class. Default: createMongoAbility. */
  abilityClass?: AbilityClass<TAbility> | CreateAbility<TAbility>;

  /** Resolve the canonical TenantContext from a request. Server-side lookup. */
  resolveTenantContext: (req: unknown) => TenantContext<TId> | Promise<TenantContext<TId>>;

  /** Define rules for the resolved context. May be async. */
  defineAbilities: (
    builder: TenantAbilityBuilder<TAbility, TId>,
    ctx: TenantContext<TId>,
    req: unknown,
  ) => void | Promise<void>;

  /** Optional relationship graph; required for $relatedTo rules. */
  graph?: RelationshipGraph;

  /** Run validateTenantRules at .build() time. Default: true. */
  validateRulesAtBuild?: boolean;

  /** Predicate for non-decorator-marked public routes. */
  isPublic?: (ctx: ExecutionContext) => boolean;

  /** Auto-register the global APP_GUARD + APP_INTERCEPTOR. Default: true. */
  registerAsGlobal?: boolean;
}

See also