Console Branding Architecture

Audience: engineers working on the Console UI or Authority branding APIs, and operators customizing per-tenant branding. In one line: Authority stores a tenant-scoped branding record (logo, favicon, title, theme tokens); the Console applies it at runtime, falling back to built-in Stella Ops defaults when no record exists.

1. Purpose

NOTE (reconciled against src/): the branding fallback default is hard-coded in the Angular BrandingService (tenantId: 'default', title: 'Stella Ops', empty themeTokens) — it is not injected from config.json. Authority returns the same StellaOps default when a tenant has no stored branding record.

2. Scope

Non-goals:

3. Branding Data Model

Authority stores a tenant-scoped branding record. The persisted shape is the StoredBrandingSettings record in ConsoleBrandingEndpointExtensions.cs:

NOTE (reconciled against src/): there is no separate brandingId; the record is identified by the tenant slug. The API/storage field names are logoUri/faviconUri (not logo/favicon). themeTokens is a single flat CSS-variable map — light/dark/high-contrast are not modelled as separate maps in the persisted record.

Constraints (enforced in Authority UpdateBranding):

NOTE (reconciled against src/): MIME-type restriction (image/svg+xml, image/png, image/jpeg) is not validated server-side. It is a client-only constraint enforced by the file-input accept attribute in the branding editor — and the accepted set differs by asset: the logo input accepts image/png,image/jpeg,image/svg+xml while the favicon input accepts image/x-icon,image/png.

Persistence contract:

4. Configuration Layering

  1. Built-in defaults — hard-coded in the Angular BrandingService (Stella Ops, empty theme tokens). (Reconciled: not sourced from /config.json.)
  2. Tenant branding from Authority (GET /console/branding?tenantId=<slug>) after the active tenant is known.
  3. Session/preview overrides — applied locally by the editor’s live preview (computed signals + previewColor); not persisted and not round-tripped through the server preview endpoint.

If Authority is unreachable (or the request errors), the BrandingService falls back to the built-in defaults via its catchError path, unless the caller passes fallbackToDefault: false.

5. API Surface

All endpoints are served by Authority (ConsoleBrandingEndpointExtensions.MapConsoleBrandingEndpoints). The /console/admin/branding group requires the ui.admin scope (group-level) and a tenant header; each admin endpoint then layers its own scope, and mutation routes add fresh authentication individually.

Tenant context: the public read endpoint takes a tenantId query parameter; the admin endpoints take the tenant from the X-StellaOps-TenantId header (AuthorityHttpHeaders.Tenant).

5.1 Read branding (public)

5.2 Read branding with metadata (admin)

5.3 Update branding (admin only)

5.4 Preview branding (admin)

ORPHANED (reconciled against src/): the server POST /console/admin/branding/preview endpoint currently has no UI consumer. The Angular BrandingService only calls the public read, the admin read (fetchAdminBranding), and the update (updateBranding); the editor’s preview is rendered entirely client-side. The endpoint remains available for API clients.

6. UI Application

RECONCILED — server/UI whitelist divergence (verify before relying on full-family persistence): the client applyThemeTokens whitelist accepts ten prefixes — --theme-bg-, --theme-text-, --theme-border-, --theme-brand-, --theme-sidebar-, --theme-status-, --theme-severity-, --theme-action-, --theme-focus-, --theme-selection-. The server SanitizeThemeTokens whitelist (Authority) is narrower — only six prefixes: --theme-bg-, --theme-text-, --theme-border-, --theme-brand-, --theme-status-, --theme-focus-. As a result, --theme-sidebar-*, --theme-severity-*, --theme-action-*, and --theme-selection-* tokens render in the live preview but are dropped by Authority on save and will not survive a reload from the persisted record. (FLAGGED as a code-level inconsistency, not a doc-only fix.)

7. Audit and Offline

RECONCILED (event name): there is no authority.branding.updated event; the emitted event is authority.admin.branding.update (see list above).

NOT IMPLEMENTED (roadmap): “branding bundles exported with a detached signature for offline import” and “Console shows last applied branding hash for verification” are not present in src/. There is no branding export/import or detached-signature path, and the branding editor does not surface the metadata.hash to operators (the API returns it, but the editor does not render it). Treat these two bullets as forward-looking until implemented. The per-record hash is computed and persisted today, and is used for canonical-record integrity / cache invalidation rather than an offline bundle.

8. References