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

@nestarc/tenancy

v0.4.0

Published

Multi-tenancy module for NestJS with PostgreSQL Row Level Security (RLS) and Prisma support

Readme

@nestarc/tenancy

npm version CI codecov License: MIT

Multi-tenancy module for NestJS with PostgreSQL Row Level Security (RLS) and Prisma support.

One line of code. Automatic tenant isolation.

Features

  • RLS-based isolation — PostgreSQL enforces tenant boundaries at the database level
  • AsyncLocalStorage — Zero-overhead request-scoped tenant context (no REQUEST scope)
  • Prisma Client Extensions — Automatic set_config() before every query
  • 5 built-in extractors — Header, Subdomain, JWT Claim, Path, Composite (fallback chain)
  • Lifecycle hooksonTenantResolved / onTenantNotFound for logging, auditing, custom error handling
  • Auto-inject tenant ID — Optionally inject tenant_id into create / createMany / upsert operations
  • Shared models — Whitelist models that skip RLS (e.g., Country, Currency)
  • withoutTenant() — programmatic bypass for background jobs and admin queries
  • tenancyTransaction() — interactive transaction support with RLS
  • Fail-Closed modefailClosed: true blocks model queries without tenant context, preventing accidental data exposure
  • Testing utilitiesTestTenancyModule, withTenant(), expectTenantIsolation() via @nestarc/tenancy/testing
  • Event system — optional @nestjs/event-emitter integration for tenant.resolved, tenant.not_found, etc.
  • CLI scaffoldingnpx @nestarc/tenancy init generates RLS policies and module config
  • ccTLD-aware subdomain extraction — accurate parsing for .co.uk, .co.jp, .com.au, etc.
  • SQL injection safeset_config() with bind parameters, plus UUID validation by default
  • NestJS 10 & 11 compatible, Prisma 5 & 6 compatible

Performance

Measured with PostgreSQL 16, Prisma 6, 1005 rows, 500 iterations on Apple Silicon:

| Scenario | Avg | P50 | P95 | P99 | |----------|-----|-----|-----|-----| | Direct query (no extension, 1005 rows) | 3.74ms | 3.07ms | 6.13ms | 10.44ms | | findMany with extension (402 rows via RLS) | 2.91ms | 2.66ms | 4.59ms | 6.52ms | | findFirst with extension (1 row via RLS) | 1.23ms | 1.20ms | 1.62ms | 2.00ms |

The batch transaction overhead (set_config + query) is negligible — RLS reduces the returned row count, which often makes queries faster than unfiltered equivalents.

Reproduce: docker compose up -d && npx ts-node benchmarks/rls-overhead.ts

Prerequisites

  • Node.js >= 18
  • NestJS 10 or 11
  • Prisma 5 or 6
  • PostgreSQL (with RLS support)

Installation

npm install @nestarc/tenancy

Quick Start

1. Enable RLS on your PostgreSQL tables

Every table that needs tenant isolation must have a tenant_id column and an RLS policy:

-- Ensure your table has a tenant_id column
ALTER TABLE users ADD COLUMN tenant_id TEXT NOT NULL;

-- Enable RLS
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- Create isolation policy
CREATE POLICY tenant_isolation ON users
  USING (tenant_id = current_setting('app.current_tenant', true)::text);

-- The `true` parameter means missing_ok: returns '' instead of error when unset.
-- This ensures queries without tenant context return 0 rows (not an error).
-- Repeat for each tenant-scoped table

Critical: RLS only applies to non-superuser roles. Create a dedicated application role:

CREATE ROLE app_user LOGIN PASSWORD 'your_password';
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON your_table TO app_user;

Use this role's connection string in your application. If you connect as a superuser, RLS policies are silently bypassed.

2. Register the module

import { TenancyModule } from '@nestarc/tenancy';

@Module({
  imports: [
    TenancyModule.forRoot({
      tenantExtractor: 'X-Tenant-Id', // header name
    }),
  ],
})
export class AppModule {}

3. Extend your Prisma client

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { TenancyService, createPrismaTenancyExtension } from '@nestarc/tenancy';

@Injectable()
export class PrismaService implements OnModuleInit {
  public readonly client;

  constructor(private readonly tenancyService: TenancyService) {
    const prisma = new PrismaClient();
    this.client = prisma.$extends(
      createPrismaTenancyExtension(tenancyService),
    );
  }

  async onModuleInit() {
    await this.client.$connect();
  }
}

Extension Options

createPrismaTenancyExtension(tenancyService, {
  dbSettingKey: 'app.current_tenant',  // PostgreSQL setting key (default)
  autoInjectTenantId: true,            // Auto-inject tenant_id on create/upsert
  tenantIdField: 'tenant_id',          // Column name for tenant ID (default)
  sharedModels: ['Country', 'Currency'], // Models that skip RLS entirely
})

| Option | Type | Default | Description | |--------|------|---------|-------------| | dbSettingKey | string | 'app.current_tenant' | PostgreSQL session variable name | | autoInjectTenantId | boolean | false | Auto-inject tenant ID into create, createMany, createManyAndReturn, upsert | | tenantIdField | string | 'tenant_id' | Column name to inject tenant ID into | | sharedModels | string[] | [] | Models that bypass RLS (no set_config, no injection) | | failClosed | boolean | false | Block queries when no tenant context is set (prevents accidental data exposure if RLS is misconfigured) | | experimentalTransactionSupport | boolean | false | Experimental. Enable transparent set_config inside interactive transactions. Relies on undocumented Prisma internals — prefer tenancyTransaction() for production use |

Important: If you customize dbSettingKey in TenancyModule.forRoot(), pass the same value to createPrismaTenancyExtension() and tenancyTransaction(). These are independent configurations that must match your PostgreSQL current_setting() calls.

Note: When using interactive transactions ($transaction(async (tx) => ...)), the set_config call runs in a separate connection. Call set_config manually as the first statement inside interactive transactions.

Interactive Transactions

The default Prisma extension wraps queries in batch transactions, which breaks inside $transaction(async (tx) => ...). Use the tenancyTransaction() helper:

import { tenancyTransaction } from '@nestarc/tenancy';

await tenancyTransaction(prisma, tenancyService, async (tx) => {
  const user = await tx.user.findFirst();
  await tx.order.create({ data: { userId: user.id } });
});

Experimental: Transparent Mode

const prisma = basePrisma.$extends(
  createPrismaTenancyExtension(tenancyService, {
    experimentalTransactionSupport: true, // opt-in
  })
);

⚠️ experimentalTransactionSupport relies on undocumented Prisma internals. It may break on Prisma upgrades. Use tenancyTransaction() for production-critical code.

4. Use it

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}

  findAll() {
    // Automatically filtered by RLS — only current tenant's data returned
    return this.prisma.client.user.findMany();
  }
}

Send requests with the tenant header:

curl -H "X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000" http://localhost:3000/users

All Prisma queries are automatically scoped to that tenant via RLS.

API

TenancyModule

// Synchronous
TenancyModule.forRoot({
  tenantExtractor: 'X-Tenant-Id',           // header name (string)
  dbSettingKey: 'app.current_tenant',        // PostgreSQL setting (default)
  validateTenantId: (id) => UUID_REGEX.test(id), // sync or async (default: UUID)
})

// Async with factory
TenancyModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    tenantExtractor: config.get('TENANT_HEADER'),
  }),
})

// Async with class
TenancyModule.forRootAsync({
  useClass: TenancyConfigService,
})

// Async with existing provider
TenancyModule.forRootAsync({
  useExisting: TenancyConfigService,
})

TenancyService

@Injectable()
export class SomeService {
  constructor(private readonly tenancy: TenancyService) {}

  doSomething() {
    const tenantOrNull = this.tenancy.getCurrentTenant();    // string | null
    const tenantId = this.tenancy.getCurrentTenantOrThrow(); // string (throws if missing)
  }
}

@CurrentTenant() Decorator

import { Controller, Get } from '@nestjs/common';
import { CurrentTenant } from '@nestarc/tenancy';

@Controller('users')
export class UsersController {
  @Get('me')
  whoAmI(@CurrentTenant() tenantId: string) {
    return { tenantId };
  }
}

@BypassTenancy() Decorator

Skip tenant enforcement on specific routes (e.g., health checks, public endpoints):

import { Controller, Get } from '@nestjs/common';
import { BypassTenancy } from '@nestarc/tenancy';

@Controller('health')
export class HealthController {
  @BypassTenancy()
  @Get()
  check() {
    return { status: 'ok' }; // No tenant header required
  }
}

Programmatic Bypass

Use withoutTenant() to clear the tenant context so the Prisma extension skips set_config(). With RLS enabled, this means queries return 0 rows — RLS blocks access when no tenant session variable is set.

// Background job — clears tenant context, Prisma extension skips set_config()
// With RLS enabled, queries return 0 rows (RLS blocks access when no tenant is set)
const result = await tenancyService.withoutTenant(async () => {
  return prisma.user.findMany(); // Returns 0 rows when RLS is active
});

withoutTenant() is primarily useful for:

  • Shared tables (models listed in sharedModels) — RLS is not applied, so all rows are returned
  • Tenant lookup during login — e.g., looking up a tenant record before the tenant context is established
  • Code that uses a separate admin connection — see below

To actually query across all tenants, you need one of:

  1. A superuser/RLS-exempt database connection — use a separate PrismaClient with admin credentials that bypasses RLS:
// adminPrisma uses a superuser connection — not subject to RLS
const allUsers = await tenancyService.withoutTenant(async () => {
  return adminPrisma.user.findMany(); // Returns ALL tenants' data
});
  1. A PostgreSQL bypass policy — add a policy that allows access when a bypass flag is set:
CREATE POLICY admin_bypass ON users
  USING (current_setting('app.bypass_rls', true) = 'on');
// @BypassTenancy() bypasses the GUARD only (no 403 error).
// If a tenant header is present, Prisma still scopes to that tenant.
// If no tenant header is present, Prisma skips set_config() entirely.
@Get('/admin/users')
@BypassTenancy()
async getAllUsers() {
  // With X-Tenant-Id header: returns that tenant's data
  // Without X-Tenant-Id header: returns 0 rows (RLS blocks)
  // For true cross-tenant access, use withoutTenant() + admin connection
  return this.prisma.user.findMany();
}

Tenant Extractors

Five built-in extractors cover common multi-tenancy patterns:

Header (default)

TenancyModule.forRoot({
  tenantExtractor: 'X-Tenant-Id', // shorthand for HeaderTenantExtractor
})

Subdomain

import { SubdomainTenantExtractor } from '@nestarc/tenancy';

TenancyModule.forRoot({
  tenantExtractor: new SubdomainTenantExtractor({
    excludeSubdomains: ['www', 'api'], // optional, defaults to ['www']
  }),
  validateTenantId: (id) => /^[a-z0-9-]+$/.test(id),
})
// tenant1.app.com → 'tenant1'

Note: Uses the psl package for accurate ccTLD parsing (installed automatically as a dependency).

JWT Claim

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

TenancyModule.forRoot({
  tenantExtractor: new JwtClaimTenantExtractor({
    claimKey: 'org_id',       // JWT payload key
    headerName: 'authorization', // optional, defaults to 'authorization'
  }),
})
// Authorization: Bearer eyJ... → payload.org_id

Security: This extractor does not verify the JWT signature. You must ensure JWT signature verification happens at the middleware level — not in a NestJS Guard.

NestJS execution order is: Middleware → Guards → Interceptors → Pipes. Since TenantMiddleware runs at the middleware stage, a NestJS Guard (e.g., @nestjs/passport AuthGuard) runs after the tenant is already resolved and cannot protect it.

Middleware ordering: TenancyModule registers TenantMiddleware globally via its own configure() call. To run JWT verification before tenant extraction, you have two options:

Option 1 (recommended) — Import an auth module before TenancyModule:

NestJS applies middleware in the order modules are initialized. If your auth middleware is registered in a module that is imported before TenancyModule, it will run first.

// auth.module.ts — registers JWT verification middleware globally
@Module({})
export class AuthModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(JwtVerifyMiddleware) // verifies signature, populates req.user
      .forRoutes('*');
  }
}

// app.module.ts — import AuthModule BEFORE TenancyModule
@Module({
  imports: [
    AuthModule,        // middleware runs first
    TenancyModule.forRoot({
      tenantExtractor: new JwtClaimTenantExtractor({ claimKey: 'org_id' }),
    }),
  ],
})
export class AppModule {}

Option 2 — Verify the JWT claim in onTenantResolved:

If you need to ensure the resolved tenant matches the authenticated user, use the onTenantResolved hook. This does not replace signature verification but lets you add an authorization check after extraction:

TenancyModule.forRoot({
  tenantExtractor: new JwtClaimTenantExtractor({ claimKey: 'org_id' }),
  onTenantResolved: (tenantId, req) => {
    // req.user is populated by an upstream auth middleware
    if (req.user?.org_id !== tenantId) {
      throw new ForbiddenException('Tenant mismatch');
    }
  },
})

Path Parameter

import { PathTenantExtractor } from '@nestarc/tenancy';

TenancyModule.forRoot({
  tenantExtractor: new PathTenantExtractor({
    pattern: '/api/tenants/:tenantId/resources',
    paramName: 'tenantId',
  }),
})
// /api/tenants/acme/resources → 'acme'

Composite (Fallback Chain)

import {
  CompositeTenantExtractor,
  HeaderTenantExtractor,
  SubdomainTenantExtractor,
  JwtClaimTenantExtractor,
} from '@nestarc/tenancy';

TenancyModule.forRoot({
  tenantExtractor: new CompositeTenantExtractor([
    new HeaderTenantExtractor('X-Tenant-Id'),
    new SubdomainTenantExtractor(),
    new JwtClaimTenantExtractor({ claimKey: 'org_id' }),
  ]),
})
// Tries each extractor in order, returns the first non-null result

Custom Extractor

import { TenantExtractor } from '@nestarc/tenancy';
import { Request } from 'express';

export class CookieTenantExtractor implements TenantExtractor {
  extract(request: Request): string | null {
    return request.cookies?.['tenant_id'] ?? null;
  }
}

Lifecycle Hooks

React to tenant resolution events without extending the middleware:

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';
  },
})

| Hook | Signature | When | |------|-----------|------| | 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

| Scenario | Status | Message | |----------|--------|---------| | Missing tenant header (no @BypassTenancy) | 403 | Tenant ID is required | | Invalid tenant ID format | 400 | Invalid tenant ID format | | Non-HTTP context (WebSocket, gRPC) | — | Guard skips (no enforcement) |

Fail-Closed Mode

By default, model queries without a tenant context pass through silently. Enable failClosed to block them:

const prisma = new PrismaClient().$extends(
  createPrismaTenancyExtension(tenancyService, {
    failClosed: true, // throws TenancyContextRequiredError if no tenant
  })
);

Queries are still allowed when:

  • The model is listed in sharedModels
  • withoutTenant() is used (explicit bypass)

Scope: failClosed applies to Prisma model operations (findMany, create, update, etc.). Raw queries ($queryRaw, $executeRaw) bypass the extension and are not covered — use parameterized set_config() manually for raw queries.

Testing Utilities

Import from @nestarc/tenancy/testing:

import { TestTenancyModule, withTenant, expectTenantIsolation } from '@nestarc/tenancy/testing';

// 1. Use TestTenancyModule in unit/integration tests (no middleware or guard)
const module = await Test.createTestingModule({
  imports: [TestTenancyModule.register()],
  providers: [MyService],
}).compile();

// 2. Run code in a tenant context
const result = await withTenant('tenant-1', () => service.findAll());

// 3. Assert tenant isolation in E2E tests
await expectTenantIsolation(prisma.user, 'tenant-a-uuid', 'tenant-b-uuid');

Event System

Optional integration with @nestjs/event-emitter. Install the package and import EventEmitterModule:

import { EventEmitterModule } from '@nestjs/event-emitter';
import { TenancyEvents } from '@nestarc/tenancy';

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    TenancyModule.forRoot({ tenantExtractor: 'x-tenant-id' }),
  ],
})
export class AppModule {}

// Listen for events anywhere in your app
@Injectable()
class TenantLogger {
  @OnEvent(TenancyEvents.RESOLVED)
  handleResolved({ tenantId }: { tenantId: string }) {
    console.log(`Tenant resolved: ${tenantId}`);
  }
}

Events: tenant.resolved, tenant.not_found, tenant.validation_failed, tenant.context_bypassed.

If @nestjs/event-emitter is not installed, events are silently skipped — no errors.

Security

  • SQL Injection: The Prisma extension uses set_config() with bind parameters via $executeRaw tagged template. This eliminates SQL injection risk at the database layer. Additionally, tenant IDs are validated by the middleware (UUID format by default).
  • Transaction-scoped: set_config(key, value, TRUE) is equivalent to SET LOCAL — scoped to the batch transaction. No cross-request leakage via connection pool.
  • Custom validators: If your tenant IDs are not UUIDs, provide a validateTenantId function that rejects any unsafe input.

Security Considerations

Tenant ID is client-supplied by default. The built-in extractors (Header, Subdomain, Path) read tenant identifiers directly from the request without verifying the caller's authorization to access that tenant.

For production use, you must add a trust boundary — verify that the authenticated user belongs to the claimed tenant. Options:

  1. Use JwtClaimTenantExtractor with a pre-validated JWT (tenant ID embedded by your auth server)
  2. Add validation in onTenantResolved hook — check the user's tenant membership
  3. Use authentication middleware before the tenancy middleware to establish trust

Without a trust boundary, any client can access any tenant's data by changing the header value.

How It Works

HTTP Request (X-Tenant-Id: 550e8400-e29b-41d4-a716-446655440000)
  → TenantMiddleware (extracts & validates tenant ID)
    → AsyncLocalStorage (stores tenant context)
      → TenancyGuard (rejects if missing, unless @BypassTenancy)
        → Your Controller / Service
          → Prisma Extension ($transaction → set_config() → query)
            → PostgreSQL RLS (automatic row filtering)

CLI

Scaffold RLS policies and module configuration from your Prisma schema:

npx @nestarc/tenancy init

This generates:

  • tenancy-setup.sql — PostgreSQL RLS policies, roles, and grants
  • tenancy.module-setup.ts — NestJS module registration code

License

MIT