TenancyJS
Integrations

Next.js

Tenant context across the Edge/Node boundary in the Next.js App Router - works with any ORM.

tenancyjs-integration-next supports the Next.js App Router. Next runs middleware on the Edge and your handlers on Node, so identity has to cross that boundary. The integration handles the hand-off - your Server Components and Route Handlers see the resolved tenant.

Works with any ORM. Prisma · Knex · TypeORM · Sequelize · Drizzle · Mongoose. Prisma is the common pairing; swap the adapter for yours.

Install

npm install tenancyjs-core tenancyjs-adapter-prisma tenancyjs-integration-next tenancyjs-identifiers @prisma/client

Set up the runtime

Three small files: the manager + resolver, the tenant-scoped client, and the tenancy runtime.

lib/tenancy/manager.ts
import { TenancyManager } from "tenancyjs-core";
import { createPrismaAdapter } from "tenancyjs-adapter-prisma";
import {
  HeaderTenantResolver,
  TenantResolutionChain,
} from "tenancyjs-identifiers";

export interface Tenant {
  readonly id: string;
}

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

// Classify each tenant-scoped Prisma model. `{}` uses the default "tenantId" field.
export const adapter = createPrismaAdapter<Tenant>({
  manager,
  tenantModels: { Order: {}, Post: {} },
});

// The resolver reads the request, then looks the tenant up in YOUR store.
export const resolver = new TenantResolutionChain<Tenant>({
  resolvers: [new HeaderTenantResolver({ headerName: "x-tenant-id" })],
  store: {
    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.teamIds.includes(tenant.id),
});
lib/db.ts
import { PrismaClient } from "@prisma/client";
import { adapter } from "./tenancy/manager";

// Inside a tenant scope this injects the tenant filter/field; outside it fails closed.
export const db = new PrismaClient().$extends(adapter.extension);
lib/tenancy/server.ts
import { createNextTenancy } from "tenancyjs-integration-next";
import { manager, resolver } from "./manager";

export const tenancy = createNextTenancy({
  manager,
  resolver,
  principal: async () => getSessionUser(), // read your session (Node runtime)
});

Resolve identity at the edge

Next runs middleware on the Edge and your handlers on Node. The edge helper encodes the request's tenant identity into a hint header that the Node runtime reads back - so identity is captured once, at the boundary:

middleware.ts
import { NextResponse } from "next/server";
import { withNextTenantHint } from "tenancyjs-integration-next/edge";

export function middleware(request: Request) {
  // Encodes host + x-tenant-id into the hint header carried to your handlers.
  const headers = withNextTenantHint(request);
  return NextResponse.next({ request: { headers } });
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

Run scoped work

Wrap a Route Handler with withRouteHandler (or a Server Action with withServerAction). The tenant is resolved from the request and the scope is opened for you:

app/orders/route.ts
import { tenancy } from "@/lib/tenancy/server";
import { db } from "@/lib/db";

export const GET = tenancy.withRouteHandler(async () => {
  const orders = await db.order.findMany(); // scoped to the current tenant
  return Response.json(orders);
});

Need to open the scope inline instead of wrapping the whole handler? Use tenancy.runWithRequest(request, () => db.order.findMany()).

See ADR-0009 for how the Edge → Node identity hand-off is implemented and why it's safe.

Use a different ORM

Static routes with no tenant should not call scoped adapters - guard them, or open a central scope explicitly.

On this page