Skip to content

Multi-tenancy

@granit/multi-tenancy defines a framework-agnostic tenant resolver abstraction — a pipeline of ordered resolvers that extract the current tenant from JWT claims, URL patterns, or other sources. @granit/react-multi-tenancy provides TenantProvider which resolves the tenant, exposes it via context, and automatically wires the X-Tenant-Id header on all HTTP requests through @granit/api-client.

This mirrors the .NET Granit.MultiTenancy module: same resolver pipeline pattern, same claim names, same header convention.

Peer dependencies: react ^19, @granit/api-client

  • Directory@granit/multi-tenancy/ Tenant types, resolver interface, JWT resolver factory (framework-agnostic)
    • @granit/react-multi-tenancy TenantProvider, useTenant hook, Keycloak resolver hook
PackageRoleDepends on
@granit/multi-tenancyTenantInfo, CurrentTenant, TenantResolver, resolveTenant(), createJwtClaimTenantResolver()
@granit/react-multi-tenancyTenantProvider, useTenant, useKeycloakTenantResolvers@granit/multi-tenancy, @granit/api-client, react
graph TD
    mt["@granit/multi-tenancy"]
    reactMt["@granit/react-multi-tenancy"]
    apiClient["@granit/api-client"]

    reactMt --> mt
    reactMt --> apiClient
import { TenantProvider } from '@granit/react-multi-tenancy';
import { useKeycloakTenantResolvers } from '@granit/react-multi-tenancy';
import { useAuth } from './auth-context';
function AppWithTenant({ children }: { children: React.ReactNode }) {
const { tokenParsed } = useAuth();
const resolvers = useKeycloakTenantResolvers({ tokenParsed });
return (
<TenantProvider resolvers={resolvers}>
{children}
</TenantProvider>
);
}
interface TenantInfo {
readonly id: string;
readonly name?: string;
}
interface CurrentTenant {
readonly isAvailable: boolean;
readonly tenantId: string | undefined;
readonly tenantName: string | undefined;
}
interface MultiTenancyOptions {
readonly isEnabled?: boolean; // default: true
readonly tenantIdClaimType?: string; // default: 'tenant_id'
readonly tenantIdHeaderName?: string; // default: 'X-Tenant-Id'
}
const DEFAULT_MULTI_TENANCY_OPTIONS: Required<MultiTenancyOptions> = {
isEnabled: true,
tenantIdClaimType: 'tenant_id',
tenantIdHeaderName: 'X-Tenant-Id',
} as const;

Interface for tenant resolution strategies. Mirrors .NET ITenantResolver.

interface TenantResolver {
readonly order: number; // lower = higher priority
readonly name: string; // for logging/debugging
resolve(): TenantInfo | null; // null = unable to resolve
}

Executes resolvers in ascending order, returning the first non-null result. Mirrors the .NET TenantResolverPipeline.

function resolveTenant(resolvers: readonly TenantResolver[]): TenantInfo | null;

Factory that creates a resolver extracting tenant ID from JWT claims.

interface JwtClaimTenantResolverOptions {
readonly tokenParsedGetter: () => Record<string, unknown> | undefined;
readonly claimType?: string; // default: 'tenant_id'
}
function createJwtClaimTenantResolver(
options: JwtClaimTenantResolverOptions
): TenantResolver;

Returns a resolver with order: 200 and name: "JwtClaimTenantResolver". Extracts tenant_id (or custom claim) and optional tenant_name from the decoded JWT. Returns null if the token is undefined, the claim is missing, or the claim is empty.

ResolverOrderSource
Custom (URL/subdomain)100Developer-defined
JWT claim200createJwtClaimTenantResolver()

First non-null result wins. The pipeline does not mutate the resolver array.

Resolves the tenant from the provided resolvers and exposes it via context. Automatically registers a tenant getter on @granit/api-client via setTenantGetter(), which adds the X-Tenant-Id header to all HTTP requests.

interface TenantProviderProps {
readonly resolvers: readonly TenantResolver[];
readonly options?: MultiTenancyOptions;
readonly children: ReactNode;
}
function TenantProvider(props: TenantProviderProps): JSX.Element;

When multi-tenancy is disabled (options.isEnabled: false) or no resolver matches, the provider exposes { isAvailable: false, tenantId: undefined, tenantName: undefined }.

Returns the CurrentTenant from the nearest TenantProvider. Throws if called outside the provider.

function useTenant(): CurrentTenant;
function TenantBadge() {
const { tenantId, tenantName, isAvailable } = useTenant();
if (!isAvailable) return null;
return <span>{tenantName ?? tenantId}</span>;
}

Creates a memoized array of tenant resolvers wired to a Keycloak token. Pass the result directly to TenantProvider.

interface UseKeycloakTenantResolversOptions {
readonly tokenParsed: Record<string, unknown> | undefined;
readonly claimType?: string; // default: 'tenant_id'
}
function useKeycloakTenantResolvers(
options: UseKeycloakTenantResolversOptions
): readonly TenantResolver[];
graph LR
    KC["Keycloak JWT"] --> Resolver["JWT Claim Resolver"]
    Resolver --> Provider["TenantProvider"]
    Provider --> Context["useTenant()"]
    Provider --> Header["X-Tenant-Id header"]
    Header --> API["@granit/api-client"]
    API --> Backend[".NET backend"]
  1. Keycloak authenticates the user and includes tenant_id in the JWT
  2. useKeycloakTenantResolvers creates a resolver wired to tokenParsed
  3. TenantProvider runs the resolver pipeline and exposes the tenant via context
  4. setTenantGetter() ensures every HTTP request includes X-Tenant-Id
  5. The .NET backend receives the header and resolves the tenant via HeaderTenantResolver
CategoryKey exportsPackage
TypesTenantInfo, CurrentTenant, MultiTenancyOptions, DEFAULT_MULTI_TENANCY_OPTIONS@granit/multi-tenancy
ResolverTenantResolver, resolveTenant(), createJwtClaimTenantResolver()@granit/multi-tenancy
ProviderTenantProvider, TenantProviderProps@granit/react-multi-tenancy
HooksuseTenant(), useKeycloakTenantResolvers()@granit/react-multi-tenancy