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

@juano-morello/nest-tenant

v0.1.0

Published

NestJS multi-tenant data isolation via PostgreSQL Row-Level Security

Readme

@juanomorello/nest-tenant

NestJS multi-tenant data isolation via PostgreSQL Row-Level Security (RLS) and Schema-per-Tenant strategies.

npm version License: MIT CI

Features

  • Row-Level Security isolation — tenant data is enforced at the PostgreSQL level via SET LOCAL set_config()
  • Multiple resolver types — extract tenant ID from HTTP headers, JWT claims, subdomains, or a custom function/class
  • AsyncLocalStorage context — tenant context propagated automatically across the entire request lifecycle
  • RLS bypass mechanism@BypassRLS() decorator for admin/cross-tenant operations with audit events
  • Tenant eventstenant.resolved, tenant.bypass, tenant.error, tenant.session.set via @nestjs/event-emitter
  • Configurable guard — global TenantGuard with @PublicRoute() exclusions, path patterns, and RegExp support
  • SAVEPOINT transactions — nested withTransaction() calls use real PostgreSQL SAVEPOINTs
  • Connection tracking — graceful shutdown waits for active connections to drain
  • Tenant ID validation — configurable max length and custom validator function
  • Multi-strategy isolation — choose RLS (default), Schema-per-Tenant, or implement a custom TenantIsolationStrategy
  • ORM adapters — first-class TypeORM, Prisma, and Drizzle integration via sub-path imports
  • PolicyBuilder DSL — fluent API for generating PostgreSQL RLS policy SQL
  • CLI scaffoldingnest-tenant init, migration:create, and policy:generate commands

Installation

npm install @juanomorello/nest-tenant

Peer Dependencies

Ensure the following are installed in your project:

npm install @nestjs/common @nestjs/core pg reflect-metadata rxjs

If using the JWT resolver, also install:

npm install jsonwebtoken

Optional Peer Dependencies

If using an ORM adapter, install the corresponding package:

| Adapter | Install | |---------|---------| | TypeORM | npm install typeorm @nestjs/typeorm | | Prisma | npm install @prisma/client | | Drizzle | npm install drizzle-orm |

Quick Start

import { Module } from '@nestjs/common';
import { Pool } from 'pg';
import { TenantModule, TenantContext } from '@juanomorello/nest-tenant';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

@Module({
  imports: [
    TenantModule.forRoot({
      resolver: { type: 'header', headerName: 'x-tenant-id' },
      database: { pool },
    }),
  ],
})
export class AppModule {}

// In any service:
@Injectable()
class OrderService {
  constructor(private readonly tenant: TenantContext) {}

  async getOrders() {
    return this.tenant.withConnection(async (conn) => {
      const result = await conn.query('SELECT * FROM orders');
      return result.rows; // automatically filtered by RLS
    });
  }
}

Every request with an x-tenant-id header will have RLS enforced — queries only return rows belonging to that tenant.

Configuration Reference

TenantModuleOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | resolver | ResolverConfig | (required) | How to extract the tenant ID from each request. See Resolver Types. | | database.pool | Pool | (required) | A pg.Pool instance. The library does not create or own this pool. | | database.sessionVariable | string | 'app.current_tenant' | PostgreSQL session variable set via set_config(). Must be in namespace.name format. | | database.maxTenantIdLength | number | 256 | Maximum allowed length for tenant IDs. Must be > 0. | | database.tenantIdValidator | (id: string) => boolean | undefined | Custom validation function. Return true to accept, false to reject. | | database.shutdownTimeoutMs | number | 5000 | Max time (ms) to wait for active connections to drain during onModuleDestroy. | | guard.enabled | boolean | true | Whether to register TenantGuard as a global guard. | | guard.excludePaths | Array<string \| RegExp> | [] | URL paths that bypass the guard. Supports exact strings, trailing * wildcards, and RegExp. | | guard.onMissingTenant | 'throw' \| 'skip' | 'throw' | Behavior when no tenant is resolved on a non-public route. | | guard.httpStatus | number | 403 | HTTP status code for TenantNotFoundError. Must be 400–599. | | bypass.auditEnabled | boolean | true | Emit tenant.bypass event when @BypassRLS() is used. | | context.propagateToLogs | boolean | false | (experimental) Add tenant_id to structured log context. Not yet implemented. | | strategy | 'rls' \| 'schema' \| TenantIsolationStrategy | 'rls' | Tenant data isolation strategy. See Isolation Strategies. | | schema.nameTemplate | string | 'tenant_{{tenantId}}' | Template for schema names. Use {{tenantId}} as placeholder. Only used when strategy is 'schema'. | | schema.autoCreate | boolean | false | Automatically create the schema if it doesn't exist. Only used when strategy is 'schema'. | | schema.sharedSchema | string | 'public' | Shared schema appended to the search path. Only used when strategy is 'schema'. |

Async Configuration

Use TenantModule.forRootAsync() when options depend on other providers:

TenantModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    resolver: { type: 'header' },
    database: {
      pool: new Pool({ connectionString: config.get('DATABASE_URL') }),
    },
  }),
});

Resolver Types

Header Resolver

Reads the tenant ID from an HTTP request header (default: x-tenant-id).

TenantModule.forRoot({
  resolver: { type: 'header', headerName: 'x-tenant-id' },
  database: { pool },
});

JWT Resolver

Extracts the tenant ID from a JWT claim. Optionally verifies the token signature.

TenantModule.forRoot({
  resolver: {
    type: 'jwt',
    claimKey: 'tenantId',       // dot-path to the claim (default: 'tenantId')
    jwtSecret: 'my-secret',     // omit to skip verification
  },
  database: { pool },
});

Subdomain Resolver

Extracts the tenant ID from the request hostname subdomain.

TenantModule.forRoot({
  resolver: {
    type: 'subdomain',
    position: 0,  // leftmost subdomain segment (default)
  },
  database: { pool },
});
// acme.example.com → tenant ID = 'acme'

Custom Resolver — Function

Provide a plain function for simple custom logic:

TenantModule.forRoot({
  resolver: {
    type: 'custom',
    resolver: (req) => req.headers['x-api-key'] as string | null,
  },
  database: { pool },
});

Custom Resolver — Class

Implement the TenantResolver interface for full dependency injection:

import { Injectable } from '@nestjs/common';
import { TenantResolver, TenantRequest } from '@juanomorello/nest-tenant';

@Injectable()
class DatabaseResolver implements TenantResolver {
  constructor(private readonly tenantRepo: TenantRepository) {}

  async resolve(request: TenantRequest): Promise<string | null> {
    const apiKey = request.headers['x-api-key'] as string;
    if (!apiKey) return null;
    const tenant = await this.tenantRepo.findByApiKey(apiKey);
    return tenant?.id ?? null;
  }
}

// Then in module config:
TenantModule.forRoot({
  resolver: { type: 'custom', resolver: DatabaseResolver },
  database: { pool },
});

Isolation Strategies

The library supports multiple tenant data isolation strategies. The default is RLS.

RLS (Default)

Uses SET LOCAL set_config() to set a PostgreSQL session variable. RLS policies filter rows based on this variable. This is the default behavior — no strategy option needed.

TenantModule.forRoot({
  resolver: { type: 'header' },
  database: { pool },
});

Schema-per-Tenant

Uses SET search_path to isolate tenants by PostgreSQL schema. Each tenant's data lives in a dedicated schema.

TenantModule.forRoot({
  resolver: { type: 'header' },
  database: { pool },
  strategy: 'schema',
  schema: {
    nameTemplate: 'tenant_{{tenantId}}',
    autoCreate: true,
  },
});

Custom Strategy

Implement the TenantIsolationStrategy interface for full control over how tenant isolation is applied to each connection.

import { TenantIsolationStrategy } from '@juanomorello/nest-tenant';
import { PoolClient } from 'pg';

class MyCustomStrategy implements TenantIsolationStrategy {
  readonly name = 'custom';

  async applyIsolation(client: PoolClient, tenantId: string): Promise<void> {
    await client.query(`SET search_path = 'tenant_${tenantId}', public`);
    await client.query(`SELECT set_config('app.current_tenant', $1, true)`, [tenantId]);
  }
}

TenantModule.forRoot({
  resolver: { type: 'header' },
  database: { pool },
  strategy: new MyCustomStrategy(),
});

Tip: The library also exports a built-in HybridStrategy that composes multiple strategies in sequence. Pass an array of TenantIsolationStrategy instances to its constructor.

ORM Adapters

Each ORM adapter is imported from a dedicated sub-path and provides a withClient() method via the TenantOrmAdapter<TClient> interface. The underlying connection has SET LOCAL (or the configured strategy) already applied.

TypeORM

import { TenantTypeOrmModule } from '@juanomorello/nest-tenant/typeorm';

@Module({
  imports: [
    TenantModule.forRoot({ /* ... */ }),
    TenantTypeOrmModule.forRoot(),
  ],
})
export class AppModule {}

Inject TenantTypeOrmAdapter in your services:

@Injectable()
class OrderService {
  constructor(private readonly tenantTypeOrm: TenantTypeOrmAdapter) {}

  async getOrders() {
    return this.tenantTypeOrm.withClient(async (queryRunner) => {
      return queryRunner.query('SELECT * FROM orders');
    });
  }
}

Prisma

import { TenantPrismaModule } from '@juanomorello/nest-tenant/prisma';

@Module({
  imports: [
    TenantModule.forRoot({ /* ... */ }),
    TenantPrismaModule.forRoot({ client: new PrismaClient() }),
  ],
})
export class AppModule {}

Inject TenantPrismaAdapter in your services:

@Injectable()
class OrderService {
  constructor(private readonly tenantPrisma: TenantPrismaAdapter) {}

  async getOrders() {
    return this.tenantPrisma.withClient(async (prisma) => {
      return prisma.order.findMany();
    });
  }
}

Drizzle

import { TenantDrizzleModule } from '@juanomorello/nest-tenant/drizzle';

@Module({
  imports: [
    TenantModule.forRoot({ /* ... */ }),
    TenantDrizzleModule.forRoot({ schema: mySchema }),
  ],
})
export class AppModule {}

Inject TenantDrizzleAdapter in your services:

@Injectable()
class OrderService {
  constructor(private readonly tenantDrizzle: TenantDrizzleAdapter) {}

  async getOrders() {
    return this.tenantDrizzle.withClient(async (db) => {
      return db.select().from(orders);
    });
  }
}

PolicyBuilder

The PolicyBuilder provides a fluent API for generating PostgreSQL RLS policy SQL.

import { PolicyBuilder } from '@juanomorello/nest-tenant/policy-builder';

const sql = PolicyBuilder
  .sessionVariable('app.current_tenant')
  .table('orders')
    .tenantColumn('tenant_id')
    .forRole('app_user')
    .build()
  .table('products')
    .tenantColumn('tenant_id')
    .build()
  .toSQL();

The toSQL() method returns an array of SQL statements (e.g., ALTER TABLE ... ENABLE ROW LEVEL SECURITY, CREATE POLICY ...).

Use .toMigration(filePath) to write the generated SQL directly to a migration file with a timestamped header.

CLI

The nest-tenant CLI provides scaffolding and code generation commands.

# Interactive wizard for project setup
npx nest-tenant init

# Generate a timestamped SQL migration with RLS policy template
npx nest-tenant migration:create <name>

# Generate RLS policies from a YAML/JSON config file
npx nest-tenant policy:generate --config <file>

Sub-path Exports

| Import Path | Description | |-------------|-------------| | @juanomorello/nest-tenant | Core module, context, guards, decorators, events, strategies | | @juanomorello/nest-tenant/typeorm | TypeORM adapter and module | | @juanomorello/nest-tenant/prisma | Prisma adapter and module | | @juanomorello/nest-tenant/drizzle | Drizzle adapter and module | | @juanomorello/nest-tenant/policy-builder | PolicyBuilder DSL |

API Reference

TenantContext (Injectable Service)

The primary service consumers inject. Provides all tenant-related operations.

| Method | Returns | Description | |--------|---------|-------------| | getId() | string | Current tenant ID. Throws TenantNotFoundError if no context. | | getIdOrNull() | string \| null | Current tenant ID, or null. Does not throw. | | hasTenant() | boolean | Whether a tenant context is active with a non-null ID. | | getConnection() | Promise<TenantScopedConnection> | Acquire a tenant-scoped connection (BEGIN + SET LOCAL already executed). Caller must call release(). | | withConnection(cb) | Promise<T> | Execute callback with auto-managed connection (BEGIN/COMMIT/ROLLBACK). | | withTransaction(cb) | Promise<T> | Execute callback in a transaction. Uses SAVEPOINTs when nested inside withConnection(). | | getBypassConnection() | Promise<PoolClient> | (deprecated) Raw connection without RLS. Use withBypassConnection() instead. | | withBypassConnection(cb) | Promise<T> | Execute callback with a bypass connection that is auto-released. Requires @BypassRLS(). |

TenantScopedConnection

Returned by getConnection(). All queries are automatically scoped by RLS.

| Member | Type | Description | |--------|------|-------------| | query(text, values?) | Promise<QueryResult<T>> | Execute a parameterized SQL query. | | release() | Promise<void> | Commit the transaction and return the connection to the pool. Idempotent. | | rollback() | Promise<void> | Rollback the transaction and return the connection to the pool. | | client | PoolClient | Underlying pg.PoolClient (advanced use only). | | tenantId | string | The tenant ID this connection is scoped to. |

Decorators

| Decorator | Target | Description | |-----------|--------|-------------| | @CurrentTenant() | Parameter | Injects the current tenant ID as a route handler parameter. | | @PublicRoute() | Method / Class | Marks a route as public — skips the TenantGuard check. | | @BypassRLS() | Method | Enables RLS bypass for the request. Required for getBypassConnection() / withBypassConnection(). |

Guards

| Guard | Description | |-------|-------------| | TenantGuard | Global guard ensuring tenant context on non-public routes. Registered automatically when guard.enabled is true (default). |

Events

Subscribe to tenant lifecycle events via @nestjs/event-emitter:

| Event Constant | Payload Type | Description | |----------------|-------------|-------------| | TENANT_RESOLVED | TenantResolvedEvent | Tenant ID successfully extracted from a request. | | TENANT_BYPASS | TenantBypassEvent | A @BypassRLS() connection was acquired. | | TENANT_ERROR | TenantErrorEvent | Tenant resolution or session management failed. | | TENANT_SESSION_SET | TenantSessionSetEvent | SET LOCAL executed on a connection. |

import { OnEvent } from '@nestjs/event-emitter';
import { TENANT_RESOLVED, TenantResolvedEvent } from '@juanomorello/nest-tenant';

@Injectable()
class AuditListener {
  @OnEvent(TENANT_RESOLVED)
  handleTenantResolved(event: TenantResolvedEvent) {
    console.log(`Tenant ${event.tenantId} resolved via ${event.resolver}`);
  }
}

Error Types

| Error | HTTP Status | Description | |-------|-------------|-------------| | TenantNotFoundError | 403 (configurable) | No tenant context on a protected route. | | TenantForbiddenError | 403 | Tenant ID resolved but not permitted (reserved for future allowlist). | | TenantSessionError | 500 | SET LOCAL / set_config() failed on a database connection. |

Testing

Run Unit Tests

npm run test:unit

Run Integration Tests

Integration tests use Testcontainers and require Docker:

npm run test:integration

Run All Tests

npm test

Build

npm run build

Lint

npm run lint

Documentation Build

npm run docs:build

Documentation

Full documentation with guides, API reference, and architecture diagrams is available at the documentation site.

License

MIT