Configuration
`tenants.config.json` schema, flags, and merge rules.
Single source of truth for markets, tenants, domain → tenant maps per environment, optional flags, experiments, and merged config blobs.
Location
- Default:
tenants.config.jsonat the repo / app root. @multitenant/config:loadTenantsConfig({ cwd })(defaultprocess.cwd()).
Typical layout (names vary by stack):
Invalid files throw InvalidTenantsConfigError — same checks as npx @multitenant/cli check. See Errors.
Minimal skeleton
{
"version": 1,
"defaultEnvironment": "local",
"markets": {
"us": {
"currency": "USD",
"locale": "en-US",
"timezone": "America/New_York"
}
},
"tenants": {
"acme": {
"market": "us",
"domains": {
"local": { "acme.localhost": "acme" },
"production": { "acme.example.com": "acme" }
}
}
}
}Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
version | 1 | Yes | Config version. |
defaultEnvironment | local | development | staging | production | No | Used when runtime does not pass an environment. |
markets | Record of market definitions | Yes | Currency, locale, timezone, optional config defaults (see merge below). |
tenants | Record of tenant definitions | Yes | Each references a market and has per-env domains. |
experiments | Record | No | A/B definitions; tenant overrides must reference these keys. |
defaults | Object | No | e.g. localDomainTemplate. |
Markets
Each key is a market id. Typical fields:
currency,locale,timezone(required)locales(optional) — extra BCP 47 tags; must includelocaleif setconfig(optional) — arbitrary defaults merged into every tenant in this market before tenant-levelconfig(see Config merge)
Optional presentation / SEO / theme keys exist in the schema (labels, seo, theme) — see the Zod-driven reference in the repo if you need every field.
Tenants
Each key is a tenant id.
market(required) — must exist inmarketsdomains(required) — per-environment map: host pattern → tenant key string or{ tenant, basePath? }- Patterns: exact host (
us.example.com) or wildcard (*.us.example.com)
- Patterns: exact host (
flags—Record<string, boolean>feature toggles (see below)config,configByEnvironment— merged settings (see Config merge)paths.basePath,theme,seo,access,experiments,database(per-tenant DB env var name only — not a raw URL)
Feature flags (flags)
Server: isTenantFeatureEnabled(registry, tenantKey, name) in @multitenant/core.
Client: useTenantFlag(name) when you have a resolved tenant in context.
There is no separate features field — if a CMS says “features”, map that object into flags before validateTenantsConfig.
Config merge
Order: markets[*].config → tenants[*].config → tenants[*].configByEnvironment[environment] (for the active environment).
- Deep merge for nested plain objects; scalars / arrays are replaced (last wins).
- Conflict: at the same path, one layer has a plain object and another a non-object →
InvalidTenantsConfigError.
Effective config for app code: getTenantConfig(registry, tenantKey, environment?) or useTenantConfig() in React.
Example — support is deep-merged; tier is overridden last for production:
{
"markets": {
"us": {
"currency": "USD",
"locale": "en-US",
"timezone": "America/New_York",
"config": { "support": { "tier": "standard" } }
}
},
"tenants": {
"acme": {
"market": "us",
"domains": { "local": { "acme.localhost": "acme" } },
"config": { "support": { "email": "help@acme.test" } },
"configByEnvironment": {
"production": { "support": { "tier": "priority" } }
}
}
}
}After merge for acme in production: support.tier → priority, support.email → help@acme.test.
Validation (hard errors)
- Every
tenants[*].marketexists inmarkets - No overlapping domain patterns within the same environment
- Config merge must not hit object vs non-object clashes
- Tenant
experimentsoverrides must reference top-levelexperiments
Always run npx @multitenant/cli check in CI alongside your JSON edits.