TenancyJS
Integrations

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/client

tenancyjs-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

tenancy.ts
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.

server.ts
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 custom onError) 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.

On this page