Multi-tenant + multi-market toolkit for TypeScript apps.
In one line: resolve tenant + market from the Host / proxy headers using a single tenants.config.json, then wire Next.js (App + Pages), Express, Nest, or React with typed errors and optional identity cookies.
Intro / pitfalls / diagram: docs/WHY-MULTITENANT.md.
Quick links: Getting started · Config reference · Framework overview · docs index · Release / publish
Docs site: apps/site — Next.js + Fumadocs (MDX in content/docs/); full integrator guides + examples (not just GitHub links). Local: npm run site:dev. Production: Vercel Root Directory apps/site (see apps/site/vercel.json).
Hands-on: docs/INDEX.md, examples/README.md (express-minimal, next-minimal, config-smoke), npx @multitenant/cli init.
npx @multitenant/cli init --force
npx @multitenant/cli checkScaffold writes a valid tenants.config.json (and optional framework stubs with --framework next-app | next-pages | express). Then install adapters in your app, e.g. npm install @multitenant/next next react or per-package installs — see Getting started and CLI: init.
import type { EnvironmentName, TenantsConfig } from '@multitenant/core';
import { createTenantRegistry } from '@multitenant/core';
import { createTenantMiddleware } from '@multitenant/next-app';
import tenantsConfig from './tenants.config.json';
const registry = createTenantRegistry(tenantsConfig as TenantsConfig);
const env = (process.env.MULTITENANT_ENV ?? 'local') as EnvironmentName;
export const middleware = createTenantMiddleware(registry, { environment: env });See Next App Router checklist for Edge vs Node and onMissingTenant.
- Config-driven tenants/markets via
tenants.config.json - Core engine:
createTenantRegistry(config?)returns aTenantRegistrythat can resolve aResolvedTenantfrom host/headers - Framework adapters:
@multitenant/next-app– Next.js App Router middleware + server helpers@multitenant/next-pages– Next.js Pages Router HOC + API wrapper@multitenant/react–TenantProvider+ hooks@multitenant/express– Express middleware@multitenant/nest– Nest module +@Tenant()decorator
- CLI:
multitenant init– scaffoldtenants.config.json(+ optional Next/Express stubs)multitenant check– validatetenants.config.jsonmultitenant print– print tenants/markets summarymultitenant dev– local proxy with per-tenant subdomainsmultitenant cache– manage build-time request cache (view stats, invalidate per-locale)
@multitenant/core: types (TenantsConfig,ResolvedTenant,Identity, etc.),createTenantRegistry, typed errors (InvalidTenantsConfigError,DomainResolutionError,TenantNotFoundError,isMultitenantError), guards (canAccessTenant,assertAccess)@multitenant/config:loadTenantsConfig,validateTenantsConfig,resolveConfigPath@multitenant/identity: cookie encode/decode (AES-256-GCM), re-exports identity types and guards@multitenant/dev-proxy: low-level dev proxy (startDevProxy)@multitenant/cli:multitenantbinary (deprecatedtenantifyalias)@multitenant/contentful: Contentful SDK wrapper with build-time caching (inflight dedup + filesystem persistence)@multitenant/react,@multitenant/next-app,@multitenant/next-pages,@multitenant/express,@multitenant/nest: framework adapters
Validation and resolution use typed errors from @multitenant/core (InvalidTenantsConfigError, DomainResolutionError, TenantNotFoundError, …). Reference: docs/INTERNAL/errors.md.
In a consumer app (not this repo), you’ll install from npm once published, e.g.:
npm install @multitenant/core @multitenant/config @multitenant/react @multitenant/next-app @multitenant/next-pages- Define tenants.config.json at your app root:
{
"version": 1,
"defaultEnvironment": "local",
"markets": {
"us": {
"currency": "USD",
"locale": "en-US",
"timezone": "America/New_York"
}
},
"tenants": {
"us-main": {
"market": "us",
"domains": {
"local": { "us.localhost": "us-main" },
"production": { "us.example.com": "us-main" }
}
}
}
}- Load + build registry (Node entrypoint in your app):
import { loadTenantsConfig } from '@multitenant/config';
import { createTenantRegistry } from '@multitenant/core';
const config = await loadTenantsConfig({ cwd: process.cwd() });
export const tenantRegistry = createTenantRegistry(config);You can also do createTenantRegistry() in Node to auto-load <cwd>/tenants.config.json. If you’re in an edge runtime, keep passing the loaded config explicitly.
- Wire into your framework (examples below).
middleware.ts:
import type { EnvironmentName, TenantsConfig } from '@multitenant/core';
import { createTenantRegistry } from '@multitenant/core';
import { createTenantMiddleware } from '@multitenant/next-app';
import tenantsConfig from './tenants.config.json';
const registry = createTenantRegistry(tenantsConfig as TenantsConfig);
const env = (
process.env.MULTITENANT_ENV ??
process.env.TENANTIFY_ENV ??
'local'
) as EnvironmentName;
export const middleware = createTenantMiddleware(registry, {
environment: env,
});Note: if you run next dev directly (Host doesn't match any tenant domains), createTenantMiddleware will passthrough by default (no tenant headers added). If you want strict resolution, pass onMissingTenant: 'throw'.
app/layout.tsx (async request APIs; requireTenant throws TenantNotFoundError when unresolved — aligns with react-ssr checklist):
import type { EnvironmentName } from '@multitenant/core';
import type { ReactNode } from 'react';
import { TenantProvider } from '@multitenant/react';
import { requireTenant } from '@multitenant/next-app';
import { headers } from 'next/headers';
import { tenantRegistry } from './tenant-registry';
const env = (
process.env.MULTITENANT_ENV ??
process.env.TENANTIFY_ENV ??
'local'
) as EnvironmentName;
export default async function RootLayout({ children }: { children: ReactNode }) {
const h = await headers();
const tenant = requireTenant(h, tenantRegistry, { environment: env });
return (
<html lang="en">
<body>
<TenantProvider registry={tenantRegistry} tenant={tenant} environment={env}>
{children}
</TenantProvider>
</body>
</html>
);
}Inside components:
import { useTenant, useMarket, useTenantFlag } from '@multitenant/react';
export function Header() {
const tenant = useTenant();
const market = useMarket();
const showNewNav = useTenantFlag('showNewNav');
return (
<header>
<span>{tenant.tenantKey}</span>
<span>{market.currency}</span>
{showNewNav && <nav>…</nav>}
</header>
);
}pages/_app.tsx:
import type { AppProps } from 'next/app';
import { TenantProvider } from '@multitenant/react';
import { tenantRegistry } from '../tenant-registry';
export default function App({ Component, pageProps }: AppProps & { pageProps: { tenant: any } }) {
return (
<TenantProvider registry={tenantRegistry} tenant={pageProps.tenant}>
<Component {...pageProps} />
</TenantProvider>
);
}pages/index.tsx:
import type { GetServerSideProps } from 'next';
import { withTenantGSSP } from '@multitenant/next-pages';
import { tenantRegistry } from '../tenant-registry';
export const getServerSideProps: GetServerSideProps = withTenantGSSP(
async ({ tenant }) => {
return { props: { tenant } };
},
{ registry: tenantRegistry, environment: 'local' },
);
export default function Page({ tenant }: { tenant: any }) {
return <div>Tenant: {tenant.tenantKey}</div>;
}import express from 'express';
import { loadTenantsConfig } from '@multitenant/config';
import { createTenantRegistry } from '@multitenant/core';
import { multitenantExpress } from '@multitenant/express';
async function main() {
const app = express();
const config = await loadTenantsConfig({ cwd: process.cwd() });
const registry = createTenantRegistry(config);
app.use(multitenantExpress({ registry, environment: 'local' }));
app.get('/', (req, res) => {
if (!req.tenant) return res.status(404).send('no tenant');
res.send(`Tenant ${req.tenant.tenantKey}, market ${req.tenant.marketKey}`);
});
app.listen(3000);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});import { Module } from '@nestjs/common';
import { MultitenantModuleForRoot } from '@multitenant/nest';
import { loadTenantsConfig } from '@multitenant/config';
import { createTenantRegistry } from '@multitenant/core';
const config = await loadTenantsConfig({ cwd: process.cwd() });
const registry = createTenantRegistry(config);
@Module({
imports: [
MultitenantModuleForRoot({
registry,
environment: 'local',
}),
],
})
export class AppModule {}In a controller:
import { Controller, Get } from '@nestjs/common';
import { Tenant } from '@multitenant/nest';
import type { ResolvedTenant } from '@multitenant/core';
@Controller()
export class AppController {
@Get()
index(@Tenant() tenant: ResolvedTenant | null) {
if (!tenant) return 'no tenant';
return `Tenant ${tenant.tenantKey}`;
}
}# Validate config
npx @multitenant/cli check
# Print a summary
npx @multitenant/cli print
# Dev proxy: app on 3000, proxy on 3100
npx @multitenant/cli dev --target http://localhost:3000 --port 3100
# Auto-run app dev server
npx @multitenant/cli dev --run-devMIT — github.com/klypalskyi/multitenant · Issues · packages on npm (@multitenant/*)
npm install
npm run build
npm run dev # if/when turbo dev is wired to examples