Skip to content

Lifecycle Hooks

React to tenant resolution events without extending the middleware:

typescript
TenancyModule.forRoot({
  tenantExtractor: 'X-Tenant-Id',
  onTenantResolved: async (tenantId, req) => {
    // Runs inside AsyncLocalStorage context — getCurrentTenant() works here
    logger.info({ tenantId, path: req.path }, 'tenant resolved');
    await auditService.recordAccess(tenantId);
  },
  onTenantNotFound: (req, res) => {
    // Option 1: Observation only (return void → next() is called)
    logger.warn({ path: req.path }, 'no tenant');

    // Option 2: Block the request (throw an exception)
    throw new ForbiddenException('Tenant header required');

    // Option 3: Return 'skip' to prevent next() — use res to send your own response
    res.status(401).json({ message: 'Tenant header required' });
    return 'skip';
  },
})
HookSignatureWhen
onTenantResolved(tenantId: string, req: Request) => void | Promise<void>After successful extraction and validation
onTenantNotFound(req: Request, res: Response) => void | 'skip' | Promise<void | 'skip'>When no tenant ID could be extracted

Error Responses

ScenarioStatusMessage
Missing tenant header (no @BypassTenancy)403Tenant ID is required
Invalid tenant ID format400Invalid tenant ID format
Non-HTTP context (WebSocket, gRPC)Guard skips (no enforcement)

Tenant ID Forgery Prevention

Cross-validate the tenant ID against a secondary source to prevent header forgery:

typescript
import { JwtClaimTenantExtractor } from '@nestarc/tenancy';

TenancyModule.forRoot({
  tenantExtractor: 'X-Tenant-Id',
  // Cross-check against JWT claim — rejects if they differ
  crossCheckExtractor: new JwtClaimTenantExtractor({ claimKey: 'tenantId' }),
  onCrossCheckFailed: 'reject', // 'reject' (default) | 'log'
})

If the cross-check extractor returns null (e.g., no JWT present), validation is skipped — unauthenticated endpoints work normally. On mismatch, tenant.cross_check_failed event is emitted.

Released under the MIT License.