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/clientSet up the runtime
Three small files: the manager + resolver, the tenant-scoped client, and the tenancy runtime.
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),
});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);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:
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:
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.