[{"data":1,"prerenderedAt":113},["ShallowReactive",2],{"article-\u002Fwriting\u002Fdrupal-site-settings-over-jsonapi-consumer-20260826":3},{"id":4,"title":5,"articleType":6,"categories":7,"date":11,"description":12,"extension":13,"meta":14,"paragraphs":15,"path":108,"readingTime":109,"sitemap":110,"stem":111,"__hash__":112},"articleEntries\u002Farticles-data\u002Fdrupal-site-settings-over-jsonapi-consumer-20260826.json","Drupal site settings over JSON:API, per consumer","Blog post",[8,9,10],"Drupal","Druxt","Planet Drupal","2026-08-26T19:20:00+10:00","Run the same decoupled frontend twice with two consumer IDs and get two differently branded sites. Decoupled Settings serves the site and theme config Drupal already holds over JSON:API, with per-consumer overrides. Four years ago I guessed the answer would be Consumers plus theme settings. Turns out it was.","json",{},[16,26,32,38,70,80,93,99],{"type":17,"layout":18,"regions":19},"section","layout_onecol",{"content":20},[21,24],{"type":22,"html":23},"text_formatted","\u003Cp>In April 2022 I wrote about \u003Ca href=\"\u002Fwriting\u002Fdecoupling-configuration-config-pages-20220412\">decoupling configuration with Config Pages\u003C\u002Fa>, and ended it with a guess at where I'd go next:\u003C\u002Fp>\n\n\u003Cblockquote>\u003Cp>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.\u003C\u002Fp>\u003C\u002Fblockquote>",{"type":22,"html":25},"\u003Cp>That experiment is now a module. \u003Ca href=\"https:\u002F\u002Fwww.drupal.org\u002Fproject\u002Fdecoupled_settings\">Decoupled Settings\u003C\u002Fa> 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.\u003C\u002Fp>",{"type":17,"title":27,"layout":18,"regions":28},"What core doesn't give you",{"content":29},[30],{"type":22,"html":31},"\u003Cp>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 \u003Ccode>system.site\u003C\u002Fcode> is nobody's business.\u003C\u002Fp>\n\n\u003Cp>But a decoupled frontend still needs the boring stuff:\u003C\u002Fp>\n\n\u003Cul>\u003Cli>The site name, for the document title\u003C\u002Fli>\n\u003Cli>The slogan\u003C\u002Fli>\n\u003Cli>The logo and the favicon\u003C\u002Fli>\n\u003Cli>The front page path, so the router knows what \"home\" means\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>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.\u003C\u002Fp>",{"type":17,"title":33,"layout":18,"regions":34},"Isn't that what Config Pages is for?",{"content":35},[36],{"type":22,"html":37},"\u003Cp>Config Pages is for settings \u003Cem>you\u003C\u002Fem> invent: define a type, add the fields your app needs, done. That job hasn't changed, and it's still on my sites.\u003C\u002Fp>\n\n\u003Cp>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.\u003C\u002Fp>",{"type":17,"title":39,"layout":18,"regions":40},"How it works",{"content":41},[42,44,51,53,59,61,65,68],{"type":22,"html":43},"\u003Cp>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.\u003C\u002Fp>",{"type":45,"alt":46,"caption":47,"width":48,"height":49,"src":50},"media","The per-consumer overrides form for a consumer named Partner Portal, filtered to system.site. Columns are Setting, Inherited value, Override and Value for this consumer. system.site:name and system.site:slogan are ticked and overridden to 'Partner Portal' and 'Same code, different consumer'; langcode, page.403, page.404 and page.front are unticked and inherited.","\u003Cp>Tick a setting to override it for this consumer. Everything unticked keeps following the site.\u003C\u002Fp>",1336,1022,"\u002Fimages\u002Fwriting\u002Fdecoupled-settings-consumer-overrides.png",{"type":22,"html":52},"\u003Cp>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.\u003C\u002Fp>\n\n\u003Cp>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 \u003Ccode>system.site\u003C\u002Fcode> plus the active theme, and both of \u003Ccode>system.site\u003C\u002Fcode>'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.\u003C\u002Fp>",{"type":45,"alt":54,"caption":55,"width":56,"height":57,"src":58},"The Decoupled Settings configuration form: an Exposed configuration list containing system.site, a checkbox to expose the active theme settings, an Excluded settings box listing system.site:mail and system.site:mail_notification, and a 'What a frontend will read' table showing each setting with its global value.","\u003Cp>The exposure list, the keys it holds back, and what a frontend gets from what's left. Filtered to \u003Ccode>system.site\u003C\u002Fcode> here, so the theme's fourteen keys aren't in shot.\u003C\u002Fp>",1016,1358,"\u002Fimages\u002Fwriting\u002Fdecoupled-settings-exposure-form.png",{"type":22,"html":60},"\u003Cp>Reading it is one GET:\u003C\u002Fp>",{"type":62,"title":63,"code":64},"code","Reading settings as a consumer","curl \"https:\u002F\u002Fdrupal.example.com\u002Fjsonapi\u002Fdecoupled\u002Fsettings?consumerId=partner_frontend\"",{"type":62,"title":66,"code":67},"Abbreviated: 7 of the 23 keys, otherwise as returned","{\n  \"data\": {\n    \"type\": \"decoupled_settings--settings\",\n    \"id\": \"decoupled-settings\",\n    \"attributes\": {\n      \"settings\": {\n        \"system.site\": {\n          \"name\": \"Partner Portal\",\n          \"slogan\": \"Same code, different consumer\",\n          \"page\": { \"front\": \"\u002Fnode\" }\n        },\n        \"olivero.settings\": {\n          \"favicon\": { \"url\": \"\u002Fcore\u002Fthemes\u002Folivero\u002Ffavicon.ico\", \"use_default\": true },\n          \"logo\": { \"url\": \"\u002Fcore\u002Fthemes\u002Folivero\u002Flogo.svg\", \"use_default\": true }\n        }\n      },\n      \"consumer\": \"partner_frontend\"\n    }\n  }\n}",{"type":22,"html":69},"\u003Cp>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.\u003C\u002Fp>\n\n\u003Cp>With \u003Ca href=\"https:\u002F\u002Fwww.drupal.org\u002Fproject\u002Fsimple_oauth\">Simple OAuth\u003C\u002Fa> 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.\u003C\u002Fp>",{"type":17,"title":71,"layout":18,"regions":72},"One backend, many frontends",{"content":73},[74,76,78],{"type":22,"html":75},"\u003Cp>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.\u003C\u002Fp>",{"type":22,"html":77},"\u003Cp>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.\u003C\u002Fp>\n\n\u003Cp>A consumer gets its own name, slogan, front page and branding assets. Menus, regions, layouts and per-market content are still yours to solve.\u003C\u002Fp>",{"type":22,"html":79},"\u003Cp>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 \u003Ccode>$config.decoupledSettings['system.site'].name\u003C\u002Fcode> works anywhere in the app.\u003C\u002Fp>",{"type":17,"title":81,"layout":18,"regions":82},"Where it stands",{"content":83},[84,86,88,90],{"type":22,"html":85},"\u003Cp>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.\u003C\u002Fp>",{"type":22,"html":87},"\u003Cp>It's \u003Cstrong>1.0.0-beta2\u003C\u002Fstrong>. 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.\u003C\u002Fp>\n\n\u003Cp>Overrides live on the consumer entity, so they're content rather than config, and they stay in the database rather than travelling with \u003Ccode>drush cim\u003C\u002Fcode>. 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.\u003C\u002Fp>",{"type":22,"html":89},"\u003Cp>If any of that is useful, the beta is out now:\u003C\u002Fp>",{"type":62,"title":91,"code":92},"Install","composer require 'drupal\u002Fdecoupled_settings:^1.0@beta'\ndrush en decoupled_settings",{"type":17,"title":94,"layout":18,"regions":95},"What next?",{"content":96},[97],{"type":22,"html":98},"\u003Cp>File (Field) Paths, still next up, and the Druxt half of this behind it.\u003C\u002Fp>\n\n\u003Cp>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.\u003C\u002Fp>",{"type":17,"layout":18,"regions":100},{"content":101},[102],{"type":103,"description":104,"url":105,"gitpod":106,"drupalUrl":107},"repository","\u003Cp>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.\u003C\u002Fp>","https:\u002F\u002Fgithub.com\u002FDecipher\u002Fdecoupled_settings",false,"https:\u002F\u002Fwww.drupal.org\u002Fproject\u002Fdecoupled_settings","\u002Fwriting\u002Fdrupal-site-settings-over-jsonapi-consumer-20260826","4 min",{"loc":108},"articles-data\u002Fdrupal-site-settings-over-jsonapi-consumer-20260826","AfWvvQeLG8E0HK9wKF51FKaoMngbUKR6PfbLJ8v2H6s",1787740729805]