Installation
Start with the CLI - it scaffolds your tenancy for your stack. Or wire it up by hand.
Requirements
- Node.js 24+ - the toolkit and CLI use modern Node features, and the CLI loads your TypeScript config via Node's native type-stripping (no transpiler needed).
- TypeScript is recommended but not required.
Two independent choices: framework × ORM
TenancyJS composes from two axes that don't need to know about each other:
- An integration for your framework - Express, Next.js, NestJS, or AdonisJS.
- An adapter for your ORM - Prisma, Knex, Lucid, TypeORM, Sequelize, Drizzle, or Mongoose.
Integrations and adapters compose through the same TenancyManager; they do not import each other.
That makes combinations such as Express + Sequelize, Next.js + Knex, and NestJS + Mongoose
architecturally valid. Not every Cartesian pairing has its own dedicated E2E yet, so use the documented
adapter capability plus the integration lifecycle evidence and add an application-level isolation test
for your exact pairing.
Framework and ORM support are orthogonal contracts. The capability matrix records database/strategy proof; the testing guide shows how to prove your exact application pairing.
Fastest path: scaffold with the CLI
tenancyjs-cli init writes a working tenancy.config.ts, tenant registration, and request middleware for
you - no boilerplate.
npx tenancyjs-cli initToday it ships ready-made templates for these six popular combinations:
| Framework | ORM | tenancyjs-cli init |
|---|---|---|
| Express | Prisma | ✅ scaffolded |
| Express | TypeORM | ✅ scaffolded |
| Express | Sequelize | ✅ scaffolded |
| Express | Drizzle | ✅ scaffolded |
| AdonisJS | Lucid | ✅ scaffolded |
| Next.js | Prisma | ✅ scaffolded |
Add the packages it wired (for Express + Prisma):
npm install tenancyjs-core tenancyjs-adapter-prisma tenancyjs-integration-expressThen jump to the Quickstart.
Any other combination - Express + Knex, Next.js + Knex, NestJS + anything - isn't scaffolded yet, but it's a few lines of manual wiring. Follow manual setup below.
The CLI does more than scaffold
init is just the start. The same CLI lists, creates, migrates, provisions, and runs scripts against
your live tenants. See the full CLI reference →.
Build with AI
If you are using an AI coding assistant (like Cursor, Copilot, or Claude), you can generate a highly customized, fail-closed integration prompt for your specific stack using the interactive generator below:
Prompt generator
Pick your stack. The generator enforces capability rules and compiles a comprehensive, end-to-end integration prompt with exact API signatures, code examples, and documentation links for your AI coding assistant.
# TenancyJS Integration Agent
You are an integration agent. Add fail-closed multi-tenancy to THIS project
with TenancyJS. Integrate with whatever already exists — do NOT assume a
greenfield app.
**CRITICAL:** Do NOT invent or assume API names, option values, or enum strings
from memory. Your training data may be stale. Verify against:
- The installed package's own TypeScript types (in node_modules)
- The linked documentation pages below
- `npm view <pkg> version` for versions
Read ALL of these before writing any code (source of truth):
- https://tenancyjs.pages.dev/docs (mental model + THE RULES)
- https://tenancyjs.pages.dev/docs/concepts/limitations (what is REJECTED — read first)
- https://tenancyjs.pages.dev/docs/concepts/capability-matrix (what is proven)
- https://tenancyjs.pages.dev/docs/strategies/row-level
- https://tenancyjs.pages.dev/docs/adapters/prisma
- https://tenancyjs.pages.dev/docs/integrations/express
- https://tenancyjs.pages.dev/docs/guides/resolving-tenants
- https://tenancyjs.pages.dev/docs/guides/testing-isolation
## THE RULES (violating these = cross-tenant data leak)
- ✅ Run EVERY tenant query through the scoped client inside `manager.runWithTenant()`
- ✅ Let TenantContextError throw — it's the safety net, don't catch-and-ignore
- ❌ NEVER reach for the native client/model/connection inside a scope —
on facade-enforced tiers that's a direct cross-tenant leak
- ❌ NEVER expect raw SQL or nested writes from the facade — they're REJECTED
unless in a database-enforced scope (database-per-tenant or forced-RLS on PostgreSQL)
where `unrestricted()` gives the real client safely
- ❌ NEVER nest a scope for a different tenant inside an active tenant scope
## Step 0 — Assess the app (change nothing yet)
- Confirm Node.js >= 24
- Framework: **Express**
- ORM: **Prisma**
- Database: **PostgreSQL**
- Strategy: **Row-level (single database)**
- Inventory all data models. Mark each as **tenant-scoped** or **central/global**.
Only tenant-scoped models get registered with the adapter.
- Detect how user identity / session is determined per request (needed for authorize).
- Detect any EXISTING tenant concept (tenantId/orgId column, workspaces table) and REUSE it.
- Report the versions of all packages involved.
## Step 1 — Install & Scaffold
Reference installation guide: https://tenancyjs.pages.dev/docs/getting-started/installation
1. Install the necessary packages:
```bash
npm install tenancyjs-core tenancyjs-adapter-prisma tenancyjs-integration-express tenancyjs-identifiers
```
2. Scaffold the config and middleware using the CLI `init` command (highly recommended):
```bash
npx tenancyjs-cli init --framework express --orm prisma --strategy row-level
```
This scaffolds `tenancy.config.ts`, tenant registry integration hooks, and request middleware. Review the generated files under `src/tenancy/` or `lib/tenancy/` before customizing.
## Step 2 — Create the TenancyManager (one shared instance)
```ts
import { TenancyManager } from "tenancyjs-core";
interface Tenant { readonly id: string; /* your fields */ }
export const manager = new TenancyManager<Tenant>();
```
The manager uses AsyncLocalStorage. Tenant records are frozen inside a scope.
Bootstrappers revert in reverse order on every path (including errors) via try/finally.
## Step 3 — Wire the adapter
### Prisma row-level on PostgreSQL (two paths)
**Path A — Facade extension (simpler, facade-enforced):**
```ts
import { createPrismaAdapter } from "tenancyjs-adapter-prisma";
const adapter = createPrismaAdapter({
manager,
tenantModels: { Order: {}, Post: {} }, // each key = Prisma model name
});
const db = new PrismaClient().$extends(adapter.extension);
```
Raw SQL, nested writes/reads are REJECTED. Use the scoped model API only.
**Path B — RLS-backed (database-enforced, recommended for production):**
```ts
import { createPrismaRowLevelTenancy } from "tenancyjs-adapter-prisma";
const tenancy = createPrismaRowLevelTenancy({
manager,
client, // PrismaClient with @prisma/adapter-pg driver adapter
tables: [{ model: "post", table: "posts", tenantColumn: "tenant_id" }],
});
await tenancy.validate(); // checks ENABLE + FORCE RLS and policy at startup
```
Usage: `await tenancy.run(async (tx) => tx.post.findMany())`
Full raw SQL is safe here — the database enforces isolation via forced RLS.
**Required RLS DDL (apply via `tenancy policy --apply` or manually):**
```sql
CREATE ROLE app_runtime LOGIN NOSUPERUSER NOBYPASSRLS;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts FORCE ROW LEVEL SECURITY;
CREATE POLICY posts_tenant_isolation ON posts
USING (current_setting('tenancyjs.is_central', true) = 'true'
OR tenant_id = nullif(current_setting('tenancyjs.tenant_id', true), ''))
WITH CHECK (current_setting('tenancyjs.is_central', true) = 'true'
OR tenant_id = nullif(current_setting('tenancyjs.tenant_id', true), ''));
GRANT SELECT, INSERT, UPDATE, DELETE ON posts TO app_runtime;
```
### Generating & Applying RLS Policies via the CLI (recommended)
Reference policy generator: https://tenancyjs.pages.dev/docs/cli#generate-rls-policy-sql
Instead of writing RLS policies by hand in SQL DDL:
1. Define a privileged `admin` Pool in your `tenancy.config.ts`:
```ts
import { Pool } from "pg";
// In defineTenancyRuntime:
admin: new Pool({ connectionString: process.env.ADMIN_DATABASE_URL }),
```
2. Generate and apply RLS policies automatically to your database:
```bash
npx tenancyjs-cli policy --table <t1> --table <t2> --role app_runtime --apply
```
## Step 4 — Wire the framework integration
### Express integration
```ts
import { createExpressTenancyMiddleware } from "tenancyjs-integration-express";
app.use(createExpressTenancyMiddleware({
manager,
resolver, // a TenantResolutionChain — NOT a function
principal: (req) => req.user, // optional
}));
```
## Step 5 — Scope data access
- Route ALL tenant queries through the scoped client (drop manual WHERE tenant_id = …)
- Keep admin/cross-tenant work in explicit central context:
`await manager.runInCentralContext(() => db.tenant.findMany())`
- Add error handling for resolution failures:
use `describeTenantResolutionFailure(outcome.status)` → `{ status, message }`
## Conformance & adversarial testing (Harness Guide)
Reference testing-isolation guide: https://tenancyjs.pages.dev/docs/guides/testing-isolation
Write a two-tenant adversarial test on a real test database (never app data) that plugs into the official tenancyjs-testing contract suite:
```ts
// Implement the adapter conformance testing contract:
import { createRowLevelAdapterContract } from "tenancyjs-testing";
const contractCases = createRowLevelAdapterContract(async () => {
// Propose a harness satisfying RowLevelAdapterContractHarness:
return {
async reset() {
// Wipes/truncates the orders and posts test tables
await db.order.deleteMany();
await db.post.deleteMany();
},
async seed(records) {
// Bulk inserts RowLevelAdapterContractRecord[] to test tables
for (const r of records) {
await db.post.create({
data: { id: r.id, value: r.value, tenantId: r.tenantId }
});
}
},
async create(input) {
// Inserts a single record and returns RowLevelAdapterContractRecord
const res = await db.post.create({
data: { id: input.id, value: input.value }
});
return { id: res.id, value: res.value, tenantId: res.tenantId };
},
async findMany() {
// Queries all records from test tables
const res = await db.post.findMany();
return res.map(r => ({ id: r.id, value: r.value, tenantId: r.tenantId }));
},
async count() {
// Returns count of records in test tables
return db.post.count();
},
async updateMany(value) {
// Updates all records to have the specified value
const res = await db.post.updateMany({ data: { value } });
return res.count;
},
async deleteMany() {
// Deletes all records from test tables
const res = await db.post.deleteMany();
return res.count;
},
async runWithTenant(tenantId, callback) {
return manager.runWithTenant({ id: tenantId }, callback);
},
async runInCentralContext(callback) {
return manager.runInCentralContext(callback);
},
async transaction(callback) {
// Runs operations inside a database transaction
return db.$transaction(async (tx) => {
// execute operations against tx query handle inside transaction
const operations = {
create: async (input) => {
const res = await tx.post.create({ data: { id: input.id, value: input.value } });
return { id: res.id, value: res.value, tenantId: res.tenantId };
},
findMany: async () => {
const res = await tx.post.findMany();
return res.map(r => ({ id: r.id, value: r.value, tenantId: r.tenantId }));
},
count: async () => tx.post.count(),
updateMany: async (value) => {
const res = await tx.post.updateMany({ data: { value } });
return res.count;
},
deleteMany: async () => {
const res = await tx.post.deleteMany();
return res.count;
}
};
return callback(operations);
});
}
};
});
// Run them using Vitest / Jest:
describe("TenancyJS Adapter Conformance Contract", () => {
for (const c of contractCases) {
it(c.name, () => c.run());
}
});
```
Verify that a query executed outside a tenant scope throws a TenantContextError (fail-closed).
Run `tenancy test:leak --test-file <path>` from the CLI to automate this.
## CLI configuration & diagnostics
Create `tenancy.config.ts` (loaded by Node 24 native type-stripping):
```ts
import { defineTenancyRuntime } from "tenancyjs-core";
export default defineTenancyRuntime({
manager,
store, // powers registry commands (list/create/suspend/…)
adapters: [...], // powers `tenant check` capability reporting
provisioner: {
provision: async (tenant) => { /* CREATE SCHEMA/DATABASE */ },
migrate: async (tenant) => { /* run ORM migrator */ },
deprovision: async (tenant) => { /* DROP — destructive */ },
},
admin, // optional — privileged pg connection for policy --apply
dispose: async () => { /* close connections */ },
});
```
Key commands: `tenancy tenant check`, `tenancy doctor`, `tenancy tenant list`,
`tenancy tenant provision <id>`, `tenancy tenant migrate --all`,
`tenancy policy --table posts --role app_runtime --apply`.
## Step 6 — Verify & deliver
1. Deliver all config files, middleware registrations, and converted routes.
2. Write a two-tenant leak test (against a TEST database — never seed app data)
proving (a) no cross-tenant read and (b) unscoped access throws.
3. Report every file changed and anything that could not be applied.
4. Give a concise rollout plan for existing data (backfill, provisioning, staged rollout).Set up your framework
Pick your framework for the complete, copy-paste wiring. Each guide works with any of the ORMs listed below it - just install that adapter and use the same steps.
Express
Prisma · Knex · TypeORM · Sequelize · Drizzle · Mongoose
Next.js
Prisma · Knex · TypeORM · Sequelize · Drizzle · Mongoose
NestJS
Prisma · Knex · TypeORM · Sequelize · Drizzle · Mongoose
AdonisJS
Lucid
Or browse by ORM adapter if you prefer.
Manual setup
Works for any framework × ORM. Install three things: the core, your framework's integration, and your ORM's adapter. For example, Express with Sequelize:
bash npm install tenancyjs-core tenancyjs-integration-express tenancyjs-adapter-sequelize bash pnpm add tenancyjs-core tenancyjs-integration-express tenancyjs-adapter-sequelize bash yarn add tenancyjs-core tenancyjs-integration-express tenancyjs-adapter-sequelize bash bun add tenancyjs-core tenancyjs-integration-express tenancyjs-adapter-sequelize Then follow the two guides for your choices - the integration guide for the framework half and the adapter guide for the ORM half. That's the whole wiring.
| Package | Role |
|---|---|
tenancyjs-core | Framework-neutral tenant context, lifecycle, and the runtime contract |
tenancyjs-identifiers | Resolve a tenant from a request (subdomain, header, path…) |
tenancyjs-adapter-prisma · -knex · -lucid · -typeorm · -sequelize · -drizzle · -mongoose | Enforce isolation inside your ORM (guides) |
tenancyjs-integration-express · -next · -adonis · -nest | Bind context to the request lifecycle (guides) |
tenancyjs-cli | The operational CLI (tenancy …) |
tenancyjs-testing | Fixtures and conformance helpers for your own tests |
TenancyJS is 0.x — safe for real use, but the API may still change before 1.0, so pin an exact
version for reproducibility.