← Tenancy

TENANCY FEATURE

Multi-Layer Tenancy

Four independent layers compose into one TenantContext, so tenant identity, locale, theme, and environment stay explicit instead of collapsing into one switch.

Organization → Locale → Theme → Environment. Each layer answers a different product question, and together they form one explicit TenantContext.

This page is not about stacking abstractions for their own sake. It shows how tenancy stops being mysterious when each decision is isolated, named, and resolved once.

Resolve organization first

The runtime must decide whose request this is before any other tenant-aware behavior makes sense.

Derive locale and theme independently

Language and branding are separate concerns. One can change without forcing the other to be hard-wired.

Keep environment as a layer, not a hidden global

Production, staging, or preview behavior should be explicit in the tenant context instead of leaking through conditionals.

Layer 1

OrganizationLayer

Identifies which tenant (organization) owns this request.

Resolvestenant_id: "acme"
StrategyOrganizationStrategy
Layer 2

LocaleLayer

Determines the preferred language for this tenant request.

Resolveslocale: "en"
StrategyLocaleStrategy
Layer 3

ThemeLayer

Loads per-tenant branding — colors, fonts, feature flags.

Resolvestheme: "acme-blue"
StrategyThemeStrategy
Layer 4

EnvironmentLayer

Identifies the deployment stage for this tenant.

Resolvesenv: "production"
StrategyEnvironmentStrategy

Organization × Theme Matrix

Once the layers are composed, each tenant gets a consistent final surface without rebuilding the rules in handlers or templates.

TenantOrganizationThemeLocaleColor
Acme Corp acme acme-theme en #1e40af
Globex Inc globex globex-theme de #166534
Initech Ltd initech initech-theme uk #c2410c

Configuring a multilayer resolver

$multilayer = new MultilayerTenantResolver([
    new LayerDefinition(new OrganizationLayer(), new OrganizationStrategy()),
    new LayerDefinition(new LocaleLayer(),       new LocaleStrategy()),
    new LayerDefinition(new ThemeLayer(),        new ThemeStrategy()),
    new LayerDefinition(new EnvironmentLayer(),  new EnvironmentStrategy()),
]);

Each layer owns one question

Organization answers who the request belongs to. Locale answers how the product speaks. Theme answers how it looks. Environment answers where it runs.

Strategies stay swappable

You can change how locale resolves without reopening organization or theme logic, because each strategy is isolated.

Consumers read one composed context

Templates and handlers should consume the final TenantContext, not reconstruct layer decisions themselves.

MultilayerTenantResolver internals

Each LayerDefinition pairs a layer type with a resolver strategy. The resolver runs all strategies, builds each layer independently, then merges them into a single immutable TenantContext.

The important architectural habit is this: downstream code reads the composed context, not the mechanics of how each layer was produced. Layers are resolved in order, but remain conceptually independent.