# Branding — Volume Mount Contract **Version:** 0.7.3 **Status:** Spec --- ## Overview Chat Switchboard supports white-label branding through a volume mount at `/branding/` on the frontend container. Deployers provide a ConfigMap (or bind mount) with their identity assets. The app reads a JSON config on startup, serves static assets at runtime, and falls back gracefully when no branding is mounted. Switchboard provides the hooks. Your brand lives in its own repo. --- ## Mount Path ``` /branding/ ← volume mount root (frontend container) ├── branding.json ← config seed (required if mount exists) ├── favicon.png ← tab/bookmark icon ├── logo.png ← splash page hero, optional sidebar └── custom.css ← style overrides (power-user escape hatch) ``` All files are optional individually, but `branding.json` is expected if the mount exists. Missing files degrade gracefully — no broken images, no JS errors. --- ## branding.json — Config Seed ```json { "org_name": "Gobha.ai", "tagline": "Something clever here", "accent_color": "#4a9eff", "logo": "logo.png", "favicon": "favicon.png" } ``` **Fields:** | Field | Type | Default | Description | |----------------|--------|----------------------|---------------------------------------------| | `org_name` | string | `"Chat Switchboard"` | Displayed in splash, header, auth card, `` | | `tagline` | string | `"Multi-Model AI Chat"` | Splash page subtitle, auth footer | | `headline` | string | `null` | Splash hero headline (default preserved if null) | | `accent_color` | string | `"#4a9eff"` | Primary UI accent (hex) | | `logo` | string | `null` | Filename relative to `/branding/` | | `favicon` | string | `null` | Filename relative to `/branding/` | | `pills` | array | (Switchboard defaults) | Feature pills on splash. `[]` = hide. See below. | **Pills format:** ```json "pills": [ { "icon": "⚡", "text": "Fast inference", "style": "accent" }, { "icon": "🔒", "text": "Zero trust", "style": "purple" }, { "icon": "🏢", "text": "Enterprise ready" } ] ``` `style` is optional: `"accent"` uses the accent color, `"purple"` uses purple, omit for the default neutral pill. Set `"pills": []` to hide the section entirely. Omit the field to keep the stock Switchboard pills. **Lifecycle:** 1. Frontend entrypoint (`docker-entrypoint-fe.sh`) reads `/branding/branding.json` on container start 2. Values are injected into `index.html` as a `<script>` block: `window.__BRANDING__` 3. The `branding` key is also upserted into `global_settings` via the backend on startup (admin panel override layer — if admins change values in the UI, those take precedence until the next cold deploy with a branding mount) 4. Frontend `initBranding()` applies values from `window.__BRANDING__` (fast, no API call) then overlays any DB overrides from `App.serverSettings.branding` (from public settings) **Resolution order:** `branding.json` (fast init) → DB `global_settings.branding` (override layer) --- ## favicon.png — Static Asset Served at `/branding/favicon.png` by nginx. The frontend `<link rel="icon">` is set dynamically by `initBranding()`: ```js // If branding favicon exists, use it; otherwise keep built-in default document.querySelector('link[rel="icon"]').href = '/branding/favicon.png'; ``` **Recommendations:** - PNG format (modern browsers prefer it over ICO) - 32×32 minimum, 192×192 recommended (covers PWA + high-DPI) - Transparent background works best with dark themes --- ## logo.png — Static Asset Served at `/branding/logo.png`. Used in: - Splash page hero (replaces the 🔀 emoji) - Sidebar header (optional, depends on size) **Recommendations:** - PNG with transparency - Max 512×512 (larger files are wasteful; displayed at ~80px on splash) - Aspect ratio: square or landscape (tall logos will be clamped) --- ## custom.css — Style Extension Loaded *after* the main stylesheet: ```html <link rel="stylesheet" href="/branding/custom.css" onerror="this.remove()"> ``` The `onerror` handler silently removes the tag if the file doesn't exist (no 404 console noise in unbrandeded deployments). **What you can do:** - Override `--accent-color` and any other CSS custom property - Change fonts (`@import` or `@font-face` with files in `/branding/`) - Add background textures or patterns - Hide elements you don't want (`display: none`) - Override specific component styles **What you shouldn't do:** - Rely on internal class names that may change between versions - Override layout properties (`flex`, `grid`) unless you're testing against the current release - Import external resources (breaks airgapped deployments) **Example:** ```css :root { --accent-color: #e74c3c; --bg-primary: #1a1a2e; } .splash-logo img { border-radius: 50%; } ``` --- ## Frontend Touchpoints These are the DOM elements and CSS properties that branding affects: | Element / Property | Default Value | Branding Source | |--------------------------|----------------------------------|-----------------------| | `<title>` | `Chat Switchboard` | `org_name` | | `.brand-text` | `Chat Switchboard` | `org_name` | | `.hero-wordmark` | `Chat Switchboard` | `org_name` | | `.hero-headline` | `One interface. Every AI model.` | `headline` | | `.hero-sub` | (default description) | `tagline` | | `.splash-logo` | SVG switchboard icon | `logo.png` → `<img>` | | `link[rel="icon"]` | `favicon-32.png` | `favicon.png` | | `--accent-color` | `#4a9eff` | `accent_color` | | `.auth-card-header h2` | `Welcome back` | `Welcome to {org_name}` | | `.auth-card-header p` | `Sign in to continue to your workspace` | `Sign in to continue` | | `.auth-footer p` | `Self-hosted AI chat...` | `tagline` | | `.hero-features` | Switchboard feature pills | `pills` array or `[]` to hide | --- ## Nginx Configuration The frontend entrypoint adds a branding location block. For path-based deployments, the block is under `BASE_PATH` so Traefik routes requests to the correct pod: ```nginx # Root deployment (no BASE_PATH): location /branding/ { alias /branding/; expires 1h; add_header Cache-Control "public"; try_files $uri =404; } # Path-based deployment (e.g. /dev, /test): location ${BASE_PATH}/branding/ { alias /branding/; expires 1h; add_header Cache-Control "public"; try_files $uri =404; } ``` Frontend JS resolves paths via `window.__BASE__ + '/branding/'` so URLs automatically include the environment prefix. Short cache (1h) so branding updates via ConfigMap rollout are picked up without requiring users to hard-refresh. --- ## K8s Deployment The frontend deployment mounts the branding ConfigMap: ```yaml spec: containers: - name: frontend volumeMounts: - name: branding mountPath: /branding readOnly: true volumes: - name: branding configMap: name: switchboard-branding optional: true # ← app works without it ``` The `optional: true` is critical — Switchboard deploys cleanly with zero branding config. The `switchboard-gobha-ai` repo (or any deployer's equivalent) creates this ConfigMap. --- ## Deployer Repo Structure (Example: switchboard-gobha-ai) ``` switchboard-gobha-ai/ ├── branding/ │ ├── branding.json │ ├── favicon.png │ ├── logo.png │ └── custom.css ├── k8s/ │ └── configmap.yaml ├── README.md └── .gitea/ └── workflows/ └── deploy.yaml ``` **configmap.yaml:** ```yaml apiVersion: v1 kind: ConfigMap metadata: name: switchboard-branding namespace: ${NAMESPACE} data: branding.json: | { "org_name": "Gobha.ai", "tagline": "Your tagline", "accent_color": "#4a9eff" } binaryData: favicon.png: <base64-encoded> logo.png: <base64-encoded> custom.css: <base64-encoded-or-use-data> ``` Note: For binary files in ConfigMaps, use `binaryData` with base64 encoding. Alternatively, use a script that creates the ConfigMap from files: ```sh kubectl create configmap switchboard-branding \ --from-file=branding/branding.json \ --from-file=branding/favicon.png \ --from-file=branding/logo.png \ --from-file=branding/custom.css \ --dry-run=client -o yaml | kubectl apply -f - ``` --- ## Backend Integration The backend participates in branding in two ways: 1. **Startup seed** (optional, future): If the backend also mounts `/branding/`, it can read `branding.json` and upsert into `global_settings` alongside the admin bootstrap. This enables admin-panel overrides without redeployment. 2. **Public settings**: The `branding` key is added to `publicSettingKeys`, making it available to non-admin users via `GET /api/v1/settings/public`. For 0.7.3, the frontend reads branding from the static mount at init time. Backend DB seeding is a future enhancement for admin-panel editing. --- ## Graceful Degradation | Condition | Behavior | |----------------------------------|-----------------------------------------------| | No `/branding/` mount | All defaults. App looks like stock Switchboard | | Mount exists, no `branding.json` | Static assets served, no text/color overrides | | `branding.json` missing fields | Each field falls back to its default | | `logo` field set, file missing | `<img>` gets 404, `onerror` shows emoji fallback | | `custom.css` missing | `<link>` tag self-removes via `onerror` | | `favicon.png` missing | Built-in favicon remains |