Problem/Motivation
Domain 2.0.x stored per-domain overrides verbatim. domain_config_ui\Config\Config::save() assigned the submitted payload to $this->data, merged it over any existing override, and saved that under the domain configuration name. Nothing was ever compared to the base configuration, so every key an editor submitted was kept, including keys whose value happened to equal the base. In effect every key was pinned.
3.x stores the sparse diff instead. Since #3587744: domain_config: Make DomainConfigOverrideEditable::save() write a sparse, cast-aware diff against base DomainConfigOverrideEditable::save() keeps only the keys whose values differ from base, and since #3593636: Empty domain config overrides are saved as {} instead of deleted, causing perpetual drush cim/cex drift an override whose diff is empty is deleted rather than written. Both changes are right for the configuration sync problems they solve, but together they drop a key whose value equals the base, and with it the difference between two intents that are not the same thing: "this domain follows the site" and "this domain holds this value". Only the second has to survive a later edit of the base.
For a per-domain theme the consequence is concrete. A domain deliberately given the same theme as the site default is stored as nothing at all, so when somebody later changes the site default that domain silently follows it, and the explicit choice is gone with no trace that it ever existed.
Steps to reproduce
The site default theme is stark. Using Domain Theme Switch, give a domain the stark theme explicitly. No override row is written, because the value matches the base. Change the site default theme to olivero. The domain now serves olivero.
Confirmed on the current 3.x head with a functional probe: immediately after the choice the override storage row does not exist, and after the site default changes the domain renders olivero rather than stark.
Why upgraded sites are hit late
The 2.x to 3.x migration copies the old payloads into the per-domain collections as they are, so keys equal to the base survive the upgrade intact and the site looks correct afterwards. They disappear on the first re-save through 3.x code, when the diff is recomputed. A domain can therefore start following the site default weeks after the upgrade, the next time somebody edits an unrelated field on that form, with nothing to connect the two events.
Proposed resolution
Let a site name the keys that must stay in the override even when their value equals the base, which is the 2.x guarantee applied to the keys where it matters rather than to everything.
- A
pinned_keyssetting on a newdomain_config.settingsobject, listing key paths per configuration name, edited on the Domain Config UI settings form. - A matching
hook_domain_config_pinned_keys_alter(), so a module can declare the keys it owns without the site having to configure them. The two sources are merged the wayisAllowedConfiguration()merges its own. save()puts the declared keys back after computing the diff, taking their values from the cast data so a pinned key carries the same value the diff would have written. Everything else stays sparse.
The factory resolves the list once and hands the keys for that configuration name to the override object. It reads the setting from the base storage rather than through the configuration factory, because this class is a configuration factory override and loading configuration through the factory here would recurse; that is the same reason the base payload already comes from the base storage.
Pinning is a named list rather than the default because save() cannot tell the two ways of writing apart. A caller using per-key set() could safely pin everything it touched, but setData() hands over the whole object, so pinning everything there would store a full copy of the configuration for every domain, which is the duplication the sparse row exists to remove.
Also in here: DomainConfigFactoryOverrideInterface::getOverrideEditable() declared @return \Drupal\Core\Config\Config while the implementation has always returned a DomainConfigOverrideEditable. Narrowed to the class actually returned. Documentation only and covariant, so no caller breaks.
Remaining tasks
Consider whether the 2.x to 3.x migration should declare the keys it migrates as pinned, which would restore the old guarantee automatically instead of asking every upgraded site to work out what to list.
User interface changes
A textarea on the Domain Config UI settings form listing the pinned keys, one configuration object per line.
API changes
A new hook_domain_config_pinned_keys_alter(). DomainConfigOverrideEditable::__construct() takes an additional optional argument carrying the resolved keys, and DomainConfigFactoryOverride takes the module handler. The narrowed @return above is documentation only.
Data model changes
A new domain_config.settings configuration object with a pinned_keys list, installed on existing sites by an update hook. Override rows written for pinned keys are ordinary override rows and round-trip through configuration sync normally.
AI-Generated: Yes (Claude Code was used to help draft this issue summary and to write the code and tests on the merge requests. I reviewed and ran the work myself before posting it.)
Issue fork domain-3618091
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #4
mably commentedComment #5
mably commentedComment #6
mably commentedComment #9
mably commented