Tenant Context

The TenantContext is the single source of truth for the active request's tenant identity. Every authorization decision in nest-warden reads from it; nothing in the library trusts a client-supplied claim directly.

Shape

interface TenantContext<TId extends TenantIdValue = string> {
  readonly tenantId: TId;
  readonly subjectId: string | number;
  readonly roles: readonly string[];
  readonly attributes?: Readonly<Record<string, unknown>>;
}
FieldPurpose
tenantIdThe canonical tenant boundary. UUID by default; integer ID also supported via the TId generic. Resolved server-side — never the raw JWT claim.
subjectIdThe acting user's stable ID. Used by rules like { id: ctx.subjectId }.
rolesThe roles active in this tenant. Drives which permission branches defineAbilities activates.
attributesFree-form bag for application-specific extras (actingNodeId, locale, feature flags). Treated as opaque by the library; rules can reference fields inside it.

Where it's set

In a NestJS app using TenantAbilityModule, the context is resolved once per request by the resolveTenantContext callback you pass to forRoot():

TenantAbilityModule.forRoot<AppAbility>({
  resolveTenantContext: async (req) => {
    const user = (req as { user: JwtPayload }).user;
    const membership = await memberships.findActive({
      userId: user.sub,
      tenantId: user.claimedTenantId,
    });
    if (!membership) throw new ForbiddenException('No active tenant membership');
    return {
      tenantId: membership.tenantId,
      subjectId: membership.userId,
      roles: membership.roles,
      attributes: { actingNodeId: membership.nodeId },
    };
  },
  defineAbilities: (builder, ctx, req) => {
    /* ... */
  },
});

Where it's read

Anywhere in your request scope, inject TenantContextService:

import { Injectable, Scope, Inject } from '@nestjs/common';
import { TenantContextService } from 'nest-warden/nestjs';

@Injectable({ scope: Scope.REQUEST })
export class MerchantsService {
  constructor(
    @Inject(TenantContextService)
    private readonly tenantContext: TenantContextService,
  ) {}

  listMine() {
    const tenantId = this.tenantContext.tenantId; // throws if unset
    // ...
  }
}

Or use the @CurrentTenant() parameter decorator on a controller:

import { CurrentTenant } from 'nest-warden/nestjs';
import type { TenantContext } from 'nest-warden';

@Get('me/tenant')
me(@CurrentTenant() ctx: TenantContext) {
  return { tenantId: ctx.tenantId, roles: ctx.roles };
}

@Get('me/id')
id(@CurrentTenant('tenantId') tenantId: string) {
  return { tenantId };
}

When it isn't set

TenantContextService.get() throws MissingTenantContextError if called before the context is resolved. This is intentional fail-closed behavior — never silently fall back to a default tenant.

The context is automatically resolved by TenantPoliciesGuard (the guard lazy-resolves it on first read, so the order of guards vs interceptors doesn't matter — see NestJS guide § ordering).

For routes marked @Public(), the resolver is skipped — anything in those handlers that calls tenantContext.get() will throw. That's the right behavior: public endpoints shouldn't read tenant scope.

Generic tenant ID type

Default is string (UUID-friendly). For projects using integer tenant IDs:

const builder = new TenantAbilityBuilder<AppAbility, number>(createMongoAbility, {
  tenantId: 42,
  subjectId: 1,
  roles: ['agent'],
});

Both string and number are first-class. The TenantIdValue type admits both; pick one per project and pass it as the second generic to TenantAbilityBuilder and TenantContext.