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

@node-tenant/tenant-monorepo

v1.0.0

Published

Enterprise Multi-Tenant Platform for Node.js

Readme

@node-tenant/tenant

Enterprise Multi-Tenant Platform for Node.js — framework-agnostic, ORM-agnostic, production-ready.

npm version License: MIT TypeScript Node.js


What is this?

@node-tenant/tenant is a complete multi-tenant ecosystem for Node.js — think Django-Tenants, but for the JavaScript world. It is not just tenant detection middleware. It is a full platform covering:

| Feature | Description | |---|---| | Tenant Lifecycle | Create, update, suspend, delete tenants | | Organizations | Group tenants under parent orgs (HiringGo → India, USA, UAE) | | Relationships | Tenant-to-tenant connections (partner, collaboration, etc.) | | Resource Sharing | Share candidates, documents, etc. across tenants with permissions | | Multiple DB Strategies | tenantId column, schema-per-tenant, database-per-tenant, or hybrid | | Framework Adapters | Express, Next.js, NestJS, Fastify | | ORM Adapters | Prisma, TypeORM, Sequelize, Mongoose | | Storage Adapters | Pluggable storage for the public control layer | | Plugin System | Extend with custom plugins and lifecycle hooks | | Event System | Subscribe to tenant.created, shared, etc. | | Permissions Engine | read / write / admin / owner per resource type | | CLI | tenant create, migrate-all, share, and more | | AI Plugin | Tenant-aware Claude AI integration (optional) |


Quick Start

Install

# Core is always required
pnpm add @node-tenant/tenant-core

# Pick your framework adapter
pnpm add @node-tenant/tenant-express   # Express
pnpm add @node-tenant/tenant-next      # Next.js
pnpm add @node-tenant/tenant-nestjs    # NestJS
pnpm add @node-tenant/tenant-fastify   # Fastify

# Pick your storage adapter (for the control layer)
pnpm add @node-tenant/tenant-storage-prisma    # Prisma
pnpm add @node-tenant/tenant-storage-typeorm   # TypeORM
pnpm add @node-tenant/tenant-storage-mongodb   # MongoDB

# Pick your ORM adapter (for tenant query scoping)
pnpm add @node-tenant/tenant-prisma    # Prisma tenant-scoped queries
pnpm add @node-tenant/tenant-typeorm   # TypeORM
pnpm add @node-tenant/tenant-mongoose  # Mongoose

Express + Prisma (60 seconds)

import express from 'express';
import { PrismaClient } from '@prisma/client';
import { TenantManager } from '@node-tenant/tenant-core';
import { tenantMiddleware, subdomainResolver } from '@node-tenant/tenant-express';
import { createPrismaStorage } from '@node-tenant/tenant-storage-prisma';
import { useTenantPrisma } from '@node-tenant/tenant-prisma';

const prisma = new PrismaClient();
const storage = createPrismaStorage(prisma);
const manager = new TenantManager({ storage });

const app = express();
app.use(express.json());

// Resolve tenant from subdomain: acme.myapp.com → slug "acme"
app.use(tenantMiddleware({ manager, resolver: subdomainResolver(storage) }));

app.get('/users', async (_req, res) => {
  // All queries are automatically scoped to the current tenant
  const db = useTenantPrisma(prisma);
  const users = await db.user.findMany(); // WHERE tenant_id = 'acme'
  res.json(users);
});

app.listen(3000);

Multi-Tenancy Strategies

1. tenantId — Shared Database

All tenants share one database. Every table has a tenant_id column.

users
  id | tenant_id | name | email

useTenantPrisma(prisma) automatically injects WHERE tenant_id = ? on every query.

Best for: small-to-medium SaaS apps, lowest infrastructure cost.

2. schema — Schema per Tenant (PostgreSQL / SQL Server)

public.tenants  ← control layer
acme.users      ← tenant A data
globex.users    ← tenant B data

useTenantPrisma(prisma) issues SET search_path TO "acme" before each query.

Best for: strong data isolation, medium complexity.

3. database — Database per Tenant

tenant_control_db  ← control layer
tenant_acme_db     ← tenant A
tenant_globex_db   ← tenant B

Best for: enterprise / regulated industries requiring strict isolation.

Hybrid Mode

const db = useTenantPrisma(prisma, {
  strategyOverrides: {
    'enterprise-tenant-id': 'database',
    'free-tier-tenant-id': 'tenantId',
  }
});

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Public Control Layer                      │
│  (Organizations, Tenants, Domains, Relationships, Shares)   │
│  StorageAdapter: Prisma / TypeORM / MongoDB / Sequelize      │
└────────────────────────┬────────────────────────────────────┘
                         │
         ┌───────────────┼───────────────┐
         ▼               ▼               ▼
   TenantManager    TenantContext    PermissionEngine
   (lifecycle)      (AsyncLocal)    (read/write/admin)
         │
   ┌─────┴──────┐
   │            │
Framework    Database
Adapters     Adapters
   │            │
Express      Prisma
Next.js      TypeORM
NestJS       Mongoose
Fastify      Raw SQL

Tenant Context

The active tenant is stored in AsyncLocalStorage and is available anywhere in the request lifecycle — no prop drilling, no dependency injection required.

import { TenantContextManager } from '@node-tenant/tenant-core';

// Inside any handler / service / repository:
const ctx = TenantContextManager.current();
// { id: 'uuid', slug: 'acme', strategy: 'tenantId', config: {} }

Or wrap arbitrary code in a tenant context:

await manager.runAsTenant('acme', async () => {
  // All code here runs as tenant "acme"
  const ctx = TenantContextManager.currentOrThrow();
});

Organizations

Group multiple tenants under one organization:

import { OrganizationManager } from '@node-tenant/tenant-organizations';

const orgManager = new OrganizationManager(manager);

const { organization, tenant } = await orgManager.createWithTenant({
  orgName: 'HiringGo',
  orgSlug: 'hiringgo',
  tenantName: 'HiringGo India',
  tenantSlug: 'hiringgo-india',
  strategy: 'tenantId',
});

// Add more tenants to the org
await manager.createTenant({
  name: 'HiringGo USA',
  slug: 'hiringgo-usa',
  organizationId: organization.id,
  strategy: 'tenantId',
  status: 'active',
  config: {},
});

// List all tenants in org
const tenants = await orgManager.getTenantsForOrg(organization.id);

Tenant Relationships

Connect tenants for cross-tenant workflows:

import { TenantRelationshipManager } from '@node-tenant/tenant-relations';

const relManager = new TenantRelationshipManager(manager);

// Create a partnership
await relManager.createBidirectional(hiringGoId, goognoId, 'partner', {
  notes: 'Joint recruitment pipeline',
});

// Check if relationship exists
const isPartner = await relManager.hasRelationship(hiringGoId, goognoId, 'partner');

// Get all related tenant IDs
const partners = await relManager.getRelatedTenantIds(hiringGoId, 'partner');

Relationship types: organization | collaboration | partner | marketplace | support


Resource Sharing

Share resources across tenants without duplicating data:

// HiringGo shares a candidate with Goognu (read-only)
const share = await manager.shareResource({
  resourceType: 'candidate',
  resourceId: 'candidate-john-doe',
  ownerTenantId: hiringGoId,
  targetTenantId: goognoId,
  permission: 'read',
});

// Check access
const canRead = await manager.canAccessResource({
  tenantId: goognoId,
  resourceType: 'candidate',
  resourceId: 'candidate-john-doe',
  required: 'read',  // true
});

const canWrite = await manager.canAccessResource({
  tenantId: goognoId,
  resourceType: 'candidate',
  resourceId: 'candidate-john-doe',
  required: 'write', // false
});

// Revoke sharing
await manager.unshareResource(share.id);

Permission hierarchy: owner > admin > write > read


Tenant Resolution

| Strategy | Example | Resolver | |---|---|---| | Subdomain | acme.myapp.com | subdomainResolver(storage) | | Domain | acme.com | domainResolver(storage) | | Path | /acme/dashboard | pathResolver(storage, 1) | | Header | X-Tenant-ID: acme | headerResolver(storage) | | Chain | First match wins | chainResolvers(...) |

import { chainResolvers, subdomainResolver, headerResolver } from '@node-tenant/tenant-core';

const resolver = chainResolvers(
  subdomainResolver(storage),  // try subdomain first
  headerResolver(storage),     // fall back to header
);

Plugin System

const auditPlugin = {
  name: 'audit-logger',
  onTenantCreate: async (tenant) => {
    await db.auditLog.create({ action: 'tenant_created', tenantId: tenant.id });
  },
  onTenantResolved: async (ctx) => {
    console.log(`Request for tenant: ${ctx.slug}`);
  },
};

manager.use(auditPlugin);

Plugin hooks: onInstall | onTenantCreate | onTenantUpdate | onTenantDelete | onTenantResolved | onTenantSwitch


Events

manager.on('tenant.created', (tenant) => { /* ... */ });
manager.on('tenant.updated', (tenant) => { /* ... */ });
manager.on('tenant.deleted', ({ tenantId }) => { /* ... */ });
manager.on('tenant.resolved', (ctx) => { /* ... */ });
manager.on('tenant.shared', (share) => { /* ... */ });
manager.on('tenant.relationship.created', (rel) => { /* ... */ });
manager.on('organization.created', (org) => { /* ... */ });

CLI

npx @node-tenant/tenant-cli tenant list
npx @node-tenant/tenant-cli tenant create --name "Acme Corp" --slug acme --strategy tenantId
npx @node-tenant/tenant-cli tenant update <id> --status suspended
npx @node-tenant/tenant-cli tenant delete <id> --confirm
npx @node-tenant/tenant-cli tenant migrate <id> --path ./migrations
npx @node-tenant/tenant-cli tenant migrate-all
npx @node-tenant/tenant-cli tenant seed <id>
npx @node-tenant/tenant-cli tenant relationships <tenantId>
npx @node-tenant/tenant-cli tenant share --resource-type candidate --resource-id cand-001 --owner <ownerTenantId> --target <targetTenantId> --permission read

Create tenant.config.ts in your project root:

import { TenantManager } from '@node-tenant/tenant-core';
import { createPrismaStorage } from '@node-tenant/tenant-storage-prisma';
import { PrismaClient } from '@prisma/client';

const manager = new TenantManager({ storage: createPrismaStorage(new PrismaClient()) });
export { manager };

AI Plugin

Tenant-aware AI powered by Claude (optional):

pnpm add @node-tenant/tenant-ai @anthropic-ai/sdk
import { TenantAI } from '@node-tenant/tenant-ai';

const ai = new TenantAI({ manager, apiKey: process.env.ANTHROPIC_API_KEY });

// Register a tenant-scoped tool
ai.registerTool({
  name: 'list_candidates',
  description: 'List candidates for the current tenant',
  inputSchema: { type: 'object', properties: { limit: { type: 'number' } } },
  execute: async (input, ctx) => {
    // ctx.id is the active tenant — data is always isolated
    return db.candidate.findMany({ where: { tenantId: ctx.id }, take: input.limit });
  },
});

// AI runs within current tenant context, never leaks cross-tenant data
await manager.runAsTenant('acme', async () => {
  const answer = await ai.ask('How many candidates do we have?');
  console.log(answer);
});

NestJS

import { TenantModule, TenantMiddlewareService, CurrentTenant } from '@node-tenant/tenant-nestjs';

@Module({
  imports: [
    TenantModule.forRoot({ manager, resolver: subdomainResolver(storage), global: true }),
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(TenantMiddlewareService).forRoutes('*');
  }
}

@Controller('me')
export class MeController {
  @Get()
  getMe(@CurrentTenant() tenant: TenantContext) {
    return tenant;
  }
}

Next.js

// middleware.ts
import { withTenant, subdomainResolver } from '@node-tenant/tenant-next';
import { manager } from './lib/tenant';

export const middleware = withTenant(
  { manager, resolver: subdomainResolver(manager.storage) },
  (_req, ctx) => {
    const response = NextResponse.next();
    response.headers.set('x-tenant-id', ctx.id);
    return response;
  },
);

Packages

| Package | Description | |---|---| | @node-tenant/tenant-core | Core engine: types, context, events, manager, resolvers, permissions | | @node-tenant/tenant-express | Express middleware | | @node-tenant/tenant-next | Next.js adapter | | @node-tenant/tenant-nestjs | NestJS module + decorators | | @node-tenant/tenant-fastify | Fastify plugin | | @node-tenant/tenant-storage-prisma | Prisma storage adapter + schema | | @node-tenant/tenant-storage-typeorm | TypeORM storage adapter + entities | | @node-tenant/tenant-storage-mongodb | MongoDB storage adapter | | @node-tenant/tenant-storage-sequelize | Sequelize storage adapter | | @node-tenant/tenant-prisma | Prisma ORM tenant-scoped query proxy | | @node-tenant/tenant-typeorm | TypeORM ORM tenant-scoped repository | | @node-tenant/tenant-mongoose | Mongoose connection switcher | | @node-tenant/tenant-relations | Tenant relationship manager | | @node-tenant/tenant-organizations | Organization manager | | @node-tenant/tenant-ai | Claude AI plugin with tenant isolation | | @node-tenant/tenant-cli | CLI tool |


Security

  • Tenant isolation enforced via AsyncLocalStorage — no accidental cross-tenant leaks
  • Permission hierarchy (owner > admin > write > read) validated before resource access
  • Relationship validation — resource sharing requires an established relationship
  • Audit logging — all mutations are recorded in audit_logs
  • Status checks — suspended/inactive tenants are rejected at the middleware level
  • Context verificationcurrentOrThrow() fails fast if called outside a tenant context

Development

# Clone and install
git clone https://github.com/adpulseflow/tenant
cd tenant
pnpm install

# Build all packages
pnpm build

# Run tests
pnpm test

# Run a specific example
cd examples/express-prisma-postgres
cp .env.example .env  # add your DATABASE_URL
pnpm dev

Contributing

PRs welcome. See CONTRIBUTING.md.


License

MIT © AdPulseFlow