Multitenant

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.json at the repo / app root.
  • @multitenant/config: loadTenantsConfig({ cwd }) (default process.cwd()).

Typical layout (names vary by stack):

tenants.config.json

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

FieldTypeRequiredDescription
version1YesConfig version.
defaultEnvironmentlocal | development | staging | productionNoUsed when runtime does not pass an environment.
marketsRecord of market definitionsYesCurrency, locale, timezone, optional config defaults (see merge below).
tenantsRecord of tenant definitionsYesEach references a market and has per-env domains.
experimentsRecordNoA/B definitions; tenant overrides must reference these keys.
defaultsObjectNoe.g. localDomainTemplate.

Markets

Each key is a market id. Typical fields:

  • currency, locale, timezone (required)
  • locales (optional) — extra BCP 47 tags; must include locale if set
  • config (optional) — arbitrary defaults merged into every tenant in this market before tenant-level config (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 in markets
  • domains (required) — per-environment map: host pattern → tenant key string or { tenant, basePath? }
    • Patterns: exact host (us.example.com) or wildcard (*.us.example.com)
  • flagsRecord<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[*].configtenants[*].configtenants[*].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-objectInvalidTenantsConfigError.

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.tierpriority, support.emailhelp@acme.test.

Validation (hard errors)

  • Every tenants[*].market exists in markets
  • No overlapping domain patterns within the same environment
  • Config merge must not hit object vs non-object clashes
  • Tenant experiments overrides must reference top-level experiments

Always run npx @multitenant/cli check in CI alongside your JSON edits.

On this page