Source: ddx-documentation/adr/0019-tenant-branding.md
- **Status:** Accepted (plan `tenant-branding`, tasks 001-009, 012, 2026-10-25). Two items NEEDS_REVIEW (below). - **Extends:** [ADR 0014](/en/docs/adr/0014-unified-settings) (the five keys are ordinary registry keys). - **Uses:** [ADR 0017](/en/docs/adr/0017-bff-layered) (the public read goes through the BFF route table and `BffApiBridge`). - **Plan:** `plans/tenant-branding/` (`_index.md`, `00-contract.md`, progress `001`-`012`). ## Context Every tenant saw the Dudoxx name, logo, palette and fonts. A new tenant needs its own name, logo and theme in the portal, and the platform needs one default for the public site and `/auth`. The mobile auth screen also showed the app name twice. ## Decision 1. **Five settings keys**, category `branding`, in the existing registry: `portal.branding.displayName` (per-locale object, 1-48 chars), `.palette`, `.font`, `.illustration`, `.logo` (`{ mode, style }`). Platform default plus org override, no user tier; org-admin edits the org tier. Values are ids, never colours or font families. Table: [settings.md](/en/docs/settings) §Branding keys. 2. **Palettes are vendored registry brand files**, not runtime colours: `neutral`, `solace-grid` and `verdant-loop` sit beside `dudoxx.css` in `ddx-web/src/styles/brands/` and are selected by `<html data-brand>`. They are copies of the registry output, never hand-edited. The registry palettes were fixed upstream for WCAG AA and re-vendored; `check-contrast.mjs` now gates every brand in light and dark (248 of 248 pairs). 3. **Fonts are `next/font` pairs** chosen by `<html data-font>`: `dudoxx` (Urbanist), `editorial` (Fraunces), `serif` (Source Serif 4). A bridge in `globals.css` makes `--font-display` resolve on every palette: vendored palettes define `--token-font-heading`, not `--token-font-display`. 4. **One writer.** `BrandThemeScope` (`components/providers/BrandingProvider.tsx`) is the only code that sets `data-brand` and `data-font`. It is a layer stack: site scope (marketing and auth layouts), tenant scope (`portal/[orgSlug]`), and a `preview` layer for a draft, used by the branding wizard. A preview unmounts back to the layer below. 5. **Logo is generated on the client.** `BrandLogo` draws a monogram or wordmark SVG from the tenant name with semantic tokens; `upload` mode uses the org logo and falls back to the generated mark on error; `platform` uses `/logo.png`. `BrandIllustration` draws `signal`, `grid` or `orbit` SVG, or nothing. 6. **Public platform read.** `GET /platform/branding` returns only the five platform-tier values (strict projection, `@Public()`). `/platform/branding` is in the BFF `PUBLIC_PATHS`, so ddx-web reads it without a session through `callUpstream` (`'use cache'`, tag `platform-branding`, revalidated by a platform branding write). 7. **Portal identity fallback.** An org with no org-tier `displayName` or `logo` shows its org name and uploaded org logo, else a mark generated from the org name. The platform name and logo appear on the site scope only. 8. **Onboarding.** Create-org redirects to `<P>/settings/branding?onboarding=1`, a five-step wizard with review (org-admin only), saving the changed keys sequentially at org tier with the org version. Rejected: free colour input and per-surface colour pickers (breaks AA and the design system); a per-tenant theme table (the generic tier rows already carry the keys); presigned logo URLs (ADR 0011). ## Consequences - **NEEDS_REVIEW a — font faces.** Fraunces and Source Serif 4 are two extra `next/font` faces. They widen the three-face rule in `ddx-web/DESIGN.md` for tenant fonts only. The owner decides whether to keep the widening. - **NEEDS_REVIEW b — first paint.** The palette is applied in a client effect (the `textSize` precedent). The first hard load after a palette change can paint the previous palette once (seen on `/fr`: first load `dudoxx` with the new name, second load `neutral`). The `ddx_brand_palette` and `ddx_brand_font` cookies are written but nothing reads them yet. - **Deferred:** orphaned logo objects are not deleted (an upload stores bytes at once; leaving the wizard keeps them). `Org.brandPreset` is unused and untouched; the settings key is the runtime source. - **Open defects (progress/009.md):** the platform settings page keeps "Seed value, never saved" until reload after a save; the wizard name step pre-fills the platform name as the tenant value; the wizard shows two button sets per step; the home hero title and `site.brand.home` label stay fixed text; `verdant-loop` dark KPI cards read red-brown. - **Not browser-tested:** upload of a real PNG logo (the input accepts PNG, JPEG, WebP only; no SVG). - Gate chain unchanged: `./ddx-manage.sh i18n`, `naming`, `bff-layers`; `check-contrast.mjs` covers every vendored brand. Last reconciled: 2026-10-25 · plan tenant-branding task 010
tenant-branding, tasks 001-009, 012, 2026-10-25). Two items NEEDS_REVIEW (below).BffApiBridge).plans/tenant-branding/ (_index.md, 00-contract.md, progress 001-012).Every tenant saw the Dudoxx name, logo, palette and fonts. A new tenant needs its own name, logo and theme in the portal, and the platform needs one default for the public site and /auth. The mobile auth screen also showed the app name twice.
branding, in the existing registry: portal.branding.displayName (per-locale object, 1-48 chars), .palette, .font, .illustration, .logo ({ mode, style }). Platform default plus org override, no user tier; org-admin edits the org tier. Values are ids, never colours or font families. Table: §Branding keys.neutral, solace-grid and verdant-loop sit beside dudoxx.css in ddx-web/src/styles/brands/ and are selected by <html data-brand>. They are copies of the registry output, never hand-edited. The registry palettes were fixed upstream for WCAG AA and re-vendored; check-contrast.mjs now gates every brand in light and dark (248 of 248 pairs).next/font pairs chosen by <html data-font>: dudoxx (Urbanist), editorial (Fraunces), serif (Source Serif 4). A bridge in globals.css makes --font-display resolve on every palette: vendored palettes define --token-font-heading, not --token-font-display.BrandThemeScope (components/providers/BrandingProvider.tsx) is the only code that sets data-brand and data-font. It is a layer stack: site scope (marketing and auth layouts), tenant scope (portal/[orgSlug]), and a preview layer for a draft, used by the branding wizard. A preview unmounts back to the layer below.BrandLogo draws a monogram or wordmark SVG from the tenant name with semantic tokens; upload mode uses the org logo and falls back to the generated mark on error; platform uses /logo.png. BrandIllustration draws signal, grid or orbit SVG, or nothing.GET /platform/branding returns only the five platform-tier values (strict projection, @Public()). /platform/branding is in the BFF PUBLIC_PATHS, so ddx-web reads it without a session through callUpstream ('use cache', tag platform-branding, revalidated by a platform branding write).displayName or logo shows its org name and uploaded org logo, else a mark generated from the org name. The platform name and logo appear on the site scope only.<P>/settings/branding?onboarding=1, a five-step wizard with review (org-admin only), saving the changed keys sequentially at org tier with the org version.Rejected: free colour input and per-surface colour pickers (breaks AA and the design system); a per-tenant theme table (the generic tier rows already carry the keys); presigned logo URLs (ADR 0011).
next/font faces. They widen the three-face rule in ddx-web/DESIGN.md for tenant fonts only. The owner decides whether to keep the widening.textSize precedent). The first hard load after a palette change can paint the previous palette once (seen on /fr: first load dudoxx with the new name, second load neutral). The ddx_brand_palette and ddx_brand_font cookies are written but nothing reads them yet.Org.brandPreset is unused and untouched; the settings key is the runtime source.site.brand.home label stay fixed text; verdant-loop dark KPI cards read red-brown../ddx-manage.sh i18n, naming, bff-layers; check-contrast.mjs covers every vendored brand.Last reconciled: 2026-10-25 · plan tenant-branding task 010
adr/
deploy/
research/