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 thedomain.{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
mappingif 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 |