← all posts
2026.08.26 · 4 min

Drupal site settings over JSON:API, per consumer

DrupalDruxtPlanet Drupal

In April 2022 I wrote about decoupling configuration with Config Pages, and ended it with a guess at where I'd go next:

One of my future experiments with Druxt will be to look at the options of using the Drupal Consumers module alongside Drupal theme settings as a solution.

That experiment is now a module. Decoupled Settings serves the site and theme configuration Drupal already holds over JSON:API, and lets every frontend override just the values it needs. It went up on Drupal.org this week.

What core doesn't give you

Core's JSON:API is an entity API. Content entities and config entities alike, so node types, image styles and views are all in there. What it has never exposed is simple configuration, and there's a good argument that it shouldn't by default, because plenty of what sits in system.site is nobody's business.

But a decoupled frontend still needs the boring stuff:

  • The site name, for the document title
  • The slogan
  • The logo and the favicon
  • The front page path, so the router knows what "home" means

So every decoupled build solves it locally, and most of the ways to solve it end up keeping a second copy of the site name somewhere.

Isn't that what Config Pages is for?

Config Pages is for settings you invent: define a type, add the fields your app needs, done. That job hasn't changed, and it's still on my sites.

Where it gets awkward is settings Drupal already stores. Model the site name as a field on a Config Page and the site name exists in two places, and something has to keep them in step, and sooner or later something won't.

How it works

So the module doesn't store settings. It reads them from the config objects that already hold them, and a consumer stores only what it changes.

Tick a setting to override it for this consumer. Everything unticked keeps following the site.

Tick a setting and it's yours. Leave it alone and it keeps following the site. Tick it and leave the value blank and that frontend doesn't show a slogan at all, which is a different thing again from clearing the override and going back to inheriting.

What's exposed at all is bounded twice: the config objects you list, and within those, the keys typed config schema declares. On install that's system.site plus the active theme, and both of system.site's email keys ship excluded. The settings form shows exactly how wide the exposure is, unsaved changes included, so you can trim it to the keys you want before anything ships.

The exposure list, the keys it holds back, and what a frontend gets from what's left. Filtered to system.site here, so the theme's fourteen keys aren't in shot.

Reading it is one GET:

Reading settings as a consumer
curl "https://drupal.example.com/jsonapi/decoupled/settings?consumerId=partner_frontend"
Abbreviated: 7 of the 23 keys, otherwise as returned
{
  "data": {
    "type": "decoupled_settings--settings",
    "id": "decoupled-settings",
    "attributes": {
      "settings": {
        "system.site": {
          "name": "Partner Portal",
          "slogan": "Same code, different consumer",
          "page": { "front": "/node" }
        },
        "olivero.settings": {
          "favicon": { "url": "/core/themes/olivero/favicon.ico", "use_default": true },
          "logo": { "url": "/core/themes/olivero/logo.svg", "use_default": true }
        }
      },
      "consumer": "partner_frontend"
    }
  }
}

The logo and favicon come back as resolved URLs, through core's own theme settings resolution and its fallbacks. Responses collect the config cache tags of every object that contributed, so an editor changing the site name invalidates them with nobody writing a hook.

With Simple OAuth it's simpler still: a client credentials token identifies the consumer by itself, no header and no query parameter, because the OAuth client and the settings consumer are the same entity.

One backend, many frontends

A consumer is a name on a request. Point two builds of the same frontend at one backend, have each identify as a different consumer, and each comes up as its own site: its own name, its own slogan, its own logo and favicon. Nothing in the code differs between them, only an environment variable.

Read the settings at build time and bake them into the artifact, and the artifact doesn't depend on Drupal at runtime, at the cost of a rebuild whenever a setting changes. Read them at runtime and the cache tags start earning their keep. Either way it's one request, so it matters very little what the frontend is built with.

A consumer gets its own name, slogan, front page and branding assets. Menus, regions, layouts and per-market content are still yours to solve.

If you're on Druxt, a module that wires all of this into the build is close behind, for the current Druxt release. It identifies the app as a consumer, reads the settings, and applies the site name and favicon to the document head. It also bakes the whole resolved set into Nuxt's runtime config, so $config.decoupledSettings['system.site'].name works anywhere in the app.

Where it stands

108 tests and 492 assertions across unit, kernel and functional coverage, green on Drupal 10 and Drupal 11. The first pipeline on drupalcode.org came back green across all eight jobs on its first run, which never happens.

It's 1.0.0-beta2. Drupal's security advisory policy covers stable releases only, and the response shape can still move before 1.0. Adopt now and you're the second consumer of this API, which means you get a say in that shape while it's still soft.

Overrides live on the consumer entity, so they're content rather than config, and they stay in the database rather than travelling with drush cim. Multilingual works one way today: exposed values follow language negotiation, but an override applies in every language, so you can translate a setting or brand it per consumer, not both yet.

If any of that is useful, the beta is out now:

Install
composer require 'drupal/decoupled_settings:^1.0@beta'
drush en decoupled_settings

What next?

File (Field) Paths, still next up, and the Druxt half of this behind it.

If you're running decoupled Drupal already, how are you getting the site name into your frontend today? I'd like to know, while the API shape is still soft enough to change.

Source on GitHub, project page and issue queue on Drupal.org. If your projects lean on modules like this one, sponsoring is what keeps them actively maintained.

Discussion

via GitHub Discussions