Multitenant

React

`@multitenant/react` — TenantProvider, hooks, SSR/RSC.

React context and hooks for tenant, market, feature flags, theme, experiments, and per-tenant config — fed from server-provided ResolvedTenant plus a client-side TenantRegistry rebuilt from the same static config the server used.

TenantProvider and hooks run in the client tree ('use client' in Next).

Install

npm install @multitenant/react react

Peer: react ≥ 18.

Mental model

LayerResponsibility
Server (RSC, SSR, loader)Resolve ResolvedTenant with createTenantRegistry + adapter (getTenantFromHeaders, resolveByRequest, …).
ClientTenantProvider + useTenant, useMarket, useTenantConfig, …

ResolvedTenant is plain JSON — safe across the server→client boundary. TenantRegistry is not serializable: on the client, import the same tenants.config.json (or shared module) and call createTenantRegistry(config) once, then pass registry + tenant into TenantProvider.

Client shell (TenantProvider)

'use client';

import type { ResolvedTenant, TenantsConfig } from '@multitenant/core';
import { createTenantRegistry } from '@multitenant/core';
import { TenantProvider } from '@multitenant/react';
import tenantsConfig from '../tenants.config.json';

const registry = createTenantRegistry(tenantsConfig as TenantsConfig);

export function AppShell({
  tenant,
  children,
}: {
  tenant: ResolvedTenant;
  children: React.ReactNode;
}) {
  return (
    <TenantProvider registry={registry} tenant={tenant} environment={tenant.environment}>
      {children}
    </TenantProvider>
  );
}

Server layout passes tenant from requireTenant(headers(), registry) or equivalent — see Next.js — App Router.

Hooks

'use client';

import {
  useTenant,
  useMarket,
  useTenantFlag,
  useTenantTheme,
  useExperiment,
  useTenantConfig,
  useTenantContext,
} from '@multitenant/react';

export function TenantBanner() {
  const { tenantKey, marketKey, flags } = useTenant();
  const market = useMarket();
  const beta = useTenantFlag('beta');
  const theme = useTenantTheme();

  return (
    <div>
      {tenantKey} · {marketKey} · {market.locale} · {market.currency}
      {beta ? ' · beta' : ''}
      {theme?.preset ? ` · theme ${theme.preset}` : ''}
    </div>
  );
}

export function ExperimentSlot() {
  const variant = useExperiment('hero_copy');
  return <section data-variant={variant}>…</section>;
}

export function TenantSettingsPanel() {
  const settings = useTenantConfig<{ supportEmail?: string }>();
  return <a href={`mailto:${settings.supportEmail}`}>Support</a>;
}

/** Escape hatch: registry + tenant + environment in one object */
export function DebugTenant() {
  const { registry, tenant, environment } = useTenantContext();
  return <pre>{JSON.stringify({ environment, tenantKey: tenant.tenantKey }, null, 2)}</pre>;
}
  • useMarket()NormalizedMarket from registry.markets[tenant.marketKey] (throws if missing).
  • useTenantFlag(name) — boolean from tenant.flags[name].
  • useExperiment(key) — string variant from tenant.experiments[key] (throws if missing).
  • useTenantConfig() — merged per-tenant config via getTenantConfig(registry, tenantKey, environment) from core.

Next.js App Router

Resolve in a Server Component or layout with headers() + requireTenant, then pass tenant into a client shell that mounts TenantProvider. See Next.js — App Router.

Classic SSR

Embed ResolvedTenant in HTML/JSON bootstrap, then on the client use TenantProvider with the same static config import as the server.

See also

On this page