Express
Add multi-tenancy to Express with one middleware - works with any ORM.
tenancyjs-integration-express resolves the tenant for each request and runs that request inside the
tenant's scope. Everything downstream - routes, services, ORM queries - is automatically scoped.
Works with any ORM. Prisma · Knex · TypeORM · Sequelize · Drizzle · Mongoose. This guide uses Prisma; swap the adapter for yours (the middleware is identical).
Install
The framework integration plus your ORM's adapter (Prisma shown):
npm install tenancyjs-core tenancyjs-adapter-prisma tenancyjs-integration-express tenancyjs-identifiers @prisma/clienttenancyjs-identifiers supplies the resolver (TenantResolutionChain + the header/subdomain resolvers)
the middleware needs - it's a required part of the wiring, not optional.
Wire it up
Manager + adapter
import { TenancyManager } from "tenancyjs-core";
import { createPrismaAdapter } from "tenancyjs-adapter-prisma";
import { PrismaClient } from "@prisma/client";
export interface Tenant {
readonly id: string;
}
export const manager = new TenancyManager<Tenant>();
const adapter = createPrismaAdapter({ manager, tenantModels: { Order: {} } });
export const db = new PrismaClient().$extends(adapter.extension);The middleware
Register it early, before your routes. Give it your manager and a resolver - an object with a
resolve() method. Build one with a TenantResolutionChain from tenancyjs-identifiers: it reads the
request and looks the identifier up in your store.
import express from "express";
import { createExpressTenancyMiddleware } from "tenancyjs-integration-express";
import {
HeaderTenantResolver,
TenantResolutionChain,
} from "tenancyjs-identifiers";
import { manager, db, type Tenant } from "./tenancy";
const app = express();
const resolver = new TenantResolutionChain<Tenant>({
resolvers: [new HeaderTenantResolver({ headerName: "x-tenant-id" })],
store: {
// Look the identifier up in YOUR store; return the match(es).
async find(identifier) {
const tenant = await lookupTenant(identifier.value);
return tenant ? [{ tenant, status: "active" }] : [];
},
},
// Resolving a tenant is not authorizing it — verify membership (or opt out
// with trustResolution). See /docs/guides/resolving-tenants.
authorize: ({ tenant, principal }) =>
(principal as { teamIds: string[] }).teamIds.includes(tenant.id),
});
app.use(
createExpressTenancyMiddleware({
manager,
resolver,
principal: (req) => (req as { user?: unknown }).user,
}),
);
app.get("/orders", async (_req, res) => {
res.json(await db.order.findMany()); // already scoped to the tenant
});
// Map resolution failures to 400/404 - otherwise they surface as an unhandled 500.
app.use((err, _req, res, next) => {
if (typeof err?.statusCode === "number")
res.status(err.statusCode).json({ error: err.message });
else next(err);
});The resolver is not a (req) => … function - it's an object with resolve(input) (a
TenantResolutionChain), and it receives a ResolverInput ({ host, headers }), not the Express
req. Passing a function fails middleware validation.
How it behaves
- On success: the tenant scope is open for the whole request; adapters are scoped, and the scope is torn down when the response finishes.
- On a resolution failure: a missing/invalid identifier or an unknown/suspended tenant makes the
middleware
next(error)with an error carrying.statusCode(400/404) - add the error handler above (or pass a customonError) to turn that into a response. Your route handler never runs.
Use a different ORM
Keep the middleware; swap the adapter half. Each adapter page has a complete Express example:
Resolving the tenant
The resolver is yours - subdomain, header, path, JWT claim. For lookups against your store and consistent 400/404 semantics, see Resolving tenants.