TenancyJS
Getting Started

Quickstart

Wire tenant context into an Express + Prisma app and run your first scoped query.

This walks through the smallest useful setup: a TenancyManager, a Prisma adapter, and Express middleware that resolves the tenant per request. Using a different stack? Swap the adapter and integration imports - the shape is identical (see Adapters and Integrations).

Prefer not to hand-write this? npx tenancy init scaffolds all of it for Express + Prisma, AdonisJS + Lucid, or Next.js + Prisma. See Installation.

Create a manager

The TenancyManager owns tenant context. It uses AsyncLocalStorage, so context flows through your async calls automatically - no tenantId threading.

tenancy.ts
import { TenancyManager } from "tenancyjs-core";

export interface Tenant {
  readonly id: string;
}

export const manager = new TenancyManager<Tenant>();

Attach an adapter

An adapter enforces isolation inside your ORM. For Prisma, register the tenant-scoped models and apply the adapter's extension to your client - the extended db is what you query with. (Using a different ORM? See its adapter guide; the shape is the same.)

tenancy.ts
import { createPrismaAdapter } from "tenancyjs-adapter-prisma";
import { PrismaClient } from "@prisma/client";

const adapter = createPrismaAdapter({
  manager,
  tenantModels: { Post: {}, Order: {} },
});

// `db` is your tenant-scoped Prisma client.
export const db = new PrismaClient().$extends(adapter.extension);

Run inside a tenant scope

Everything you do through db inside runWithTenant is scoped to that tenant. Outside a scope, it fails closed - a loud error, never unscoped data.

await manager.runWithTenant({ id: "acme" }, async () => {
  const orders = await db.order.findMany(); // only acme's orders
});

// Outside a scope:
await db.order.findMany();
// ✗ TenantContextError - refuses to run unscoped

Bind it to requests

The framework integration resolves the tenant from each request and opens the scope for its lifetime, so your route handlers are automatically scoped.

server.ts
import express from "express";
import { createExpressTenancyMiddleware } from "tenancyjs-integration-express";
import { manager, db } from "./tenancy";

const app = express();

app.use(
  createExpressTenancyMiddleware({
    manager,
    resolver: (req) => ({ id: req.subdomains.at(-1) ?? "" }),
  }),
);

app.get("/orders", async (_req, res) => {
  res.json(await db.order.findMany()); // already scoped
});

If the resolver can't produce a valid tenant, the middleware responds 400/404 and your handler never runs - no unscoped request reaches your code.

Where to next

On this page