Skip to content

Domain Config

The Domain Config module provides per-domain configuration overrides. It allows each domain to have its own site name, theme settings, default front page, or any other configuration -- all from a single Drupal installation.

This module covers per-domain overrides only. Per-language overrides on top of that (keyed by (domain, langcode)) are provided by the optional Domain Config Language submodule and are described there.

Architecture: 2.x vs 3.x

Domain Config 3.x is a complete redesign of how per-domain configuration overrides are stored and resolved. The 2.x version used a custom in-house solution, while 3.x leverages Drupal's native Config Collections API.

The 2.x approach (legacy)

In Domain 2.x, per-domain overrides were stored as separate configuration objects in the default collection, using a naming convention:

domain.config.{domain_id}.{config_name}

For example, overriding the site name on one_example_com:

domain.config.one_example_com.system.site

These objects lived alongside all other configuration in the default storage. At runtime, a custom resolver would intercept configuration loads, detect the active domain, construct the override name, attempt to load it, and merge the result on top of the base configuration.

Limitations of this approach:

  • Config objects polluted the default collection namespace.
  • The naming scheme was fragile -- the regex to parse domain ID and config name out of a flat string was error-prone.
  • No integration with Drupal's ConfigFactoryOverrideInterface -- the override mechanism was entirely custom.
  • Config export/import did not understand these as overrides -- they appeared as regular configuration objects.
  • Config events (save, delete, rename) required manual propagation.

The 3.x approach (config collections)

Domain Config 3.x stores overrides in Drupal config collections -- a first-class Drupal feature designed exactly for this purpose. Collections are virtual partitions of the config storage that share the same backend (file system, database, etc.) but are logically separate.

Collection naming:

Type Format Example
Domain-only domain.{domain_id} domain.one_example_com

Within a collection, the configuration object keeps its original name. For example, the site name override for one_example_com is stored as system.site inside the domain.one_example_com collection -- not as a mangled flat name.

Key benefits:

  • Clean separation between base config and overrides.
  • Standard Drupal API (ConfigFactoryOverrideInterface).
  • Proper config export/import support -- collections are exported as subdirectories.
  • Config events (save, delete, rename) are handled automatically.

The optional Domain Config Language submodule layers a second collection (domain.{domain_id}.language.{lang_code}) on top of the per-domain one, for sites that need overrides keyed by both domain and language.

How it works at runtime

Override resolution

When Drupal loads a configuration object (e.g., system.site), the config factory asks all registered override services to provide their overrides. Domain Config registers one override service:

  • domain.config_factory_override (priority -253) -- loads the domain override from the domain.{domain_id} collection.

The final merged result follows this cascade:

Base config (default collection)
  ↓ merged with
Domain override (domain.{domain_id} collection)
  = Final runtime config

Example with system.site on domain two_example_com:

# Base config (default collection):
system.site:
  name: "My Site"

# Domain override (domain.two_example_com collection):
system.site:
  name: "Two"        # overrides "My Site" → "Two"

# Final result at runtime: name = "Two"

When domain_config_language is also installed, an extra (domain, langcode) layer is appended to this cascade -- see the Domain Config Language documentation.

Domain context

The active domain is determined by DomainNegotiationContext, which is injected into the override service. The context is set during the kernel request event by DomainSubscriber and can also be switched programmatically (e.g., by Domain Config itself when comparing configurations across domains).

When no domain is active (e.g., during Drush commands without a domain context), no overrides are applied and the base configuration is used.

Early negotiation for middlewares

If third-party middlewares need domain_config overrides before the kernel request event fires, install the Domain Early Negotiation module (domain_early_negotiation) from the Domain Extras project. See the Domain documentation for details.

Caching

The override service provides a cache suffix based on the current domain ID. This ensures that config objects cached for one domain are not served to another.

The cache metadata includes the domain cache context, so rendered output that depends on domain-specific configuration is properly varied.

Override write semantics

DomainConfigOverrideEditable::save() writes a sparse override row -- the per-domain storage holds only the keys whose values actually differ from base. This lets domain_config keep the override "in sync" with base on the Config save event by stripping identical values (see Config lifecycle events below).

The diff is computed against base after the schema cast on every save, regardless of whether the caller built the override via per-key set() calls or via setData() with a full payload. Both paths -- ConfigFormBase flows on one side, ConfigEntityStorage::doSave() on the other -- end up with the same sparse row.

A practical consequence: setting a previously overridden key back to its base value drops the key from the override row on save. Likewise, loading a config-entity with an existing override and changing a different key (e.g. saving a block layout's region/weight on a block whose label is overridden) preserves the existing override and adds the newly differing key to the row.

The override-free read path (ConfigEntityStorage::loadMultipleOverrideFree()) is the dual problem and not addressed here -- see #3587744 and the planned domain_config_entity_ui submodule in domain_extras for the read-side intervention on EntityForm-based flows.

Known limitation: sequence shrinks

Per-domain overrides on type: sequence configs cannot drop trailing items from base. This matches core's runtime semantics and is not a domain_config-specific behavior.

Example. Base has allowed_tags: ['a', 'em', 'strong', 'p'] (4 items). A user sets the per-domain override to ['a', 'em', 'strong'] (3 items, dropping 'p'). At runtime, Config::setOverriddenData() merges base + override via NestedArray::mergeDeepArray([$base, $override], TRUE). With $preserve_integer_keys=TRUE the merge walks indexed arrays per index, so base's index 3 ('p') persists in the merged result and the override appears to "not stick" -- the visitor sees the original 4-item list.

There is no representation of "drop index N from base" in the override storage format that core's merge consumes, so domain_config cannot fix this from the override side without diverging from how every other Drupal config override mechanism behaves (settings.php $config[…], module overrides, …).

Working alternatives for sequence reductions:

  • Convert the schema to an associative mapping if you control it -- domain overrides on associative maps work cleanly.
  • Override the containing config object instead, by writing the whole desired list at the parent key via custom code (out of scope for the inline UI toggle).
  • Apply the change at the base config level rather than per-domain.

Config lifecycle events

Domain Config 3.x properly handles configuration lifecycle events to keep domain overrides in sync with base configuration:

Event Behavior
Config save For each domain, if a domain override exists for the saved config, it is filtered to remove values that are identical to the new base config (keeping only actual overrides).
Config delete If the base config is deleted, the corresponding domain override is deleted from all domain collections.
Config rename If the base config is renamed, the override is renamed in all domain collections to match.

Config export and import

Because overrides live in proper Drupal collections, they integrate with the config export/import system:

Export directory structure:

Drupal's FileStorage converts dots in collection names to directory separators. The collection domain.one_example_com becomes the directory domain/one_example_com/:

config/sync/
  system.site.yml                              # Base config
  domain/
    one_example_com/
      system.site.yml                          # Domain override
    two_example_com/
      system.site.yml

Modules can also ship default domain overrides using the same convention in their config/install/ directory:

mymodule/config/install/
  domain/
    one_example_com/
      system.site.yml
    two_example_com/
      system.site.yml

When a new domain entity is created, installDomainOverrides() calls Drupal's ConfigInstallerInterface::installCollectionDefaultConfig() to install any module-provided defaults for that domain's collection.

When domain_config_language is installed, the same scheme extends with a language/{langcode}/ subdirectory under each domain folder -- see the Domain Config Language documentation.

Domain Config UI

The optional Domain Config UI module provides a user interface for managing per-domain overrides directly from existing configuration forms.

See the Domain Config UI documentation for details on:

  • Enabling/disabling overrides per configuration per domain.
  • The inline toggle on admin forms.
  • Disallowed configurations.
  • Programmatic control via alter hooks.

Migration from 2.x to 3.x

The 2.x → 3.x migration is owned by domain_config_language because every 2.x site that had per-domain overrides also had the language module enabled, and the legacy storage format was the same regardless of whether language was involved. domain_config_update_10002() auto-installs domain_config_language during drush updatedb when language is enabled, and the migration runs from domain_config_language_install().

See the Domain Config Language documentation for the full migration steps and rollback notes.

Services

Service Class Role
domain.config_factory_override DomainConfigFactoryOverride Per-domain config overrides (priority -253)
domain_config.library.discovery.collector DomainConfigLibraryDiscoveryCollector Decorates library discovery to vary by domain