GitHub - Karthick-Ramachandran/TenancyJS: Fail-closed, TypeScript-first multi-tenancy for Node.js - one tenant-isolation contract across Express, Next.js, AdonisJS & NestJS with Prisma, Knex, Lucid, TypeORM, Sequelize & Mongoose.

4 min read Original article ↗

Fail-closed, TypeScript-first multi-tenancy for Node.js.

License: MIT Node Status Isolation

One tenant contract for every framework and ORM you already use - not a replacement for them. Tenant identity rides the async execution scope, so your queries stay scoped without threading a tenantId through every call. And the guarantee that matters: any tenant-aware access without a valid context throws - it never returns another tenant's data.

await tenancy.runWithTenant(tenant, async () => {
  // Every registered adapter scopes data access to this tenant.
  // No tenant context => it throws. No silent fallback to unscoped data.
});

Read the docs first — tenancyjs.pages.dev/docs. Multi-tenancy is a security boundary: skim the Rules & limitations and Resolving tenants before you wire anything. If you use an AI assistant, run npx tenancyjs-cli init --ai-context — it writes a TENANCY.md that teaches the assistant the fail-closed rules for your stack.

0.2.0. Safe for real use; the API is still settling before 1.0 (semver 0.x). Every "supported" cell below is proven by a two-tenant adversarial isolation test on a real database - nothing is marked supported on faith.

See it in action

One command scaffolds your stack. npx tenancyjs-cli init detects your framework and ORM, asks your isolation strategy, and writes the wiring — plus an optional TENANCY.md that teaches your AI assistant the fail-closed rules.

TenancyJS init wizard

There's also a runnable two-tenant isolation demo (Express + Sequelize, database-per-tenant) in demo/: colliding primary keys stay isolated, a forgotten tenant scope throws instead of leaking, a spoofed x-tenant-id is refused with a sanitized 404, and a leak test proves it — all against a real database.

Install

npm install tenancyjs-core
# then add an adapter + framework integration for your stack, e.g.
npm install tenancyjs-adapter-prisma tenancyjs-integration-express

Three isolation strategies, one contract

Strategy What it means Adapters
Single database (row-level) Shared tables, tenant_id + forced Postgres RLS or query-scoping Knex · Lucid · Prisma · TypeORM · Sequelize · Drizzle · Mongoose
Schema per tenant One Postgres schema per tenant via search_path or a schema-bound Prisma driver client Knex · Lucid · Prisma · TypeORM · Sequelize · Drizzle
Database per tenant A separate database per tenant, routed through a bounded connection cache Knex · Lucid · Prisma · TypeORM · Sequelize · Drizzle · Mongoose

MySQL has no separate schema namespace (SCHEMA is a synonym for DATABASE), so its equivalent is database-per-tenant. MongoDB likewise supports database-per-tenant, not SQL schema-per-tenant.

Supported stacks

  • Frameworks: Express 5, Next.js (App Router), AdonisJS 7, NestJS 11 (Express or Fastify) - plus a framework-neutral core.
  • ORMs / databases: Prisma, TypeORM, Sequelize, and Drizzle (PostgreSQL + MySQL; MySQL row-level is experimental); Knex and Lucid 22 (PostgreSQL); Mongoose 9 (MongoDB replica set). Node.js 24+.

Operational CLI

tenancyjs-cli scaffolds and operates a tenancy without ever guessing about your stack. It loads your own tenancy.config.ts at runtime (Node 24 strips the types - no transpiler dependency) and acts against your live tenants:

npx tenancyjs-cli init                       # scaffold for your framework + ORM
npx tenancyjs-cli tenant check               # verify the runtime + warn on untested combos
npx tenancyjs-cli tenant list                # read your bring-your-own tenant store
npx tenancyjs-cli tenant create acme --set plan=pro
npx tenancyjs-cli tenant migrate --all       # delegate to your migrator, per tenant
npx tenancyjs-cli run ./backfill.ts --tenant acme

Registry, provisioning, and migrations all go through your store and hooks - the CLI orchestrates and fails closed, but never invents ORM behaviour it hasn't tested.

Security

Tenant identity is not authorization - your app still owns auth. TenancyJS guarantees that unknown, suspended, or ambiguous tenants never become central context, that a misbehaving tenant store cannot hand back the wrong tenant, that secrets are redacted from CLI output, and that cleanup always runs. See the security model.

Development

Requires Node.js 24+ and pnpm 10. Run the full gate with pnpm check (add TEST_DATABASE_URL / MYSQL_TEST_DATABASE_URL / TEST_MONGODB_URL to run the real-database isolation tests). Runnable examples live in a separate repo (see examples/README.md).

License

MIT - see LICENSE.