Branding Customization

Replace the GENIE.AI logo, app name, favicon, brand color, and Keycloak login screen with your institution’s identity.

This guide explains how to rebrand a GENIE.AI deployment for a partner institution: swap the logo, favicon, application title, and brand color in the web UI; replace the logo on the Keycloak login, account, admin, and email templates; and verify that everything renders correctly after a reload.

The brand surface is small (one JSON config, four logo files, two CSS files) and every file is committed to the repository, so rebranding can be a normal code change: open one MR, get one review. Because those same files are also seeded onto every deployed node, you can equally rebrand a running deployment by editing them on the node — no image rebuild, no redeploy. Pick whichever fits; Where the brand files live at runtime covers the difference, including the one trade-off that surprises people.

Overview

GENIE.AI exposes two brand surfaces:

  1. The web app (components/gov-chat-frontend/) — driven by public/config/genie-ai-config.json and src/config-loader.js. Logo, app title, favicon, and brand color live here. The brand color flows through the entire UI via the Design System --brand CSS variable.
  2. The Keycloak login + account + admin + email templates (configs/keycloak/themes/) — driven by theme properties files and FreeMarker templates. Logo, favicon, and theme colors live here. The default theme is genie; additional themes can ship next to it (see Multiple themes).

The two surfaces are independent: changing the web app’s logo does not change the Keycloak login screen, and vice versa. Plan to update both for a full rebrand.

Architecture overview. This doc is the deployer-facing “how do I do it” companion. For the deep “why one brand color drives the entire palette via OKLch” explanation, see Theme System.

Where the brand files live at runtime

Both brand surfaces are bind-mounted into their containers from the node, so they can be edited without rebuilding anything.

SurfaceRepo path (canonical)Path on a deployed node
Web app config + assetscomponents/gov-chat-frontend/public/config//opt/genieai/config/frontend/
Keycloak themes (all of them)configs/keycloak/themes//opt/genieai/config/keycloak/themes/

Node paths above use the default config_dir (/opt/genieai/config, derived from the deploy_dir group var in deploy/ansible/group_vars/all.yml). If your deployment overrides deploy_dir, substitute accordingly — the node’s rendered .env always names the exact CONFIG_DIR_FRONTEND / CONFIG_DIR_KEYCLOAK paths in use.

Both are mounted read-only inside the container, so the container never writes over your edits. The Ansible role (config_bootstrap) seeds the node directories from the repo at deploy time, and a routine ansible-playbook deploy.yml --tags deploy does it for you.

Two files are deliberately outside the mount and must not be hand-edited:

  • /app/dist/theme-fallback.css — the in-image safety net nginx serves when /config/theme-variables.css is missing. Editing it on the node does nothing; it is generated from theme-variables.css at image build time.
  • /app/dist/runtime/config.js — regenerated by the container entrypoint at every container start from environment variables. Anything you write there is overwritten on the next restart.

The edit → reload loop

  1. Edit the file on the node, for example /opt/genieai/config/frontend/genie-ai-config.json or /opt/genieai/config/keycloak/themes/genie/login/resources/img/logo.png. The runtime config directories are seeded by Ansible as root-owned 0755/0644, so editing them needs sudo:
    sudo vi /opt/genieai/config/frontend/genie-ai-config.json
    
  2. Restart only what caches. A directory bind mount is live: the frontend nginx reads the mounted files from disk on every request, so a web-app change needs nothing beyond the browser reload below. Keycloak caches themes in memory, so it is the one container that must be restarted after a theme file change:
    # Swarm — Keycloak only
    docker service update --force <stack>_keycloak
    
    In Compose mode use docker compose up -d --force-recreate keycloak instead.
  3. Reload the browser. The first time you do this after upgrading from an older deployment, do a hard reload (Ctrl+Shift+R, or Cmd+Shift+R). Browsers that visited the site before the upgrade may still hold a one-year immutable copy of these paths from the previous cache headers, and a normal reload will happily reuse it. After that first hard reload, /config/ and /runtime/config.js are served Cache-Control: no-cache, must-revalidate, so ordinary reloads are enough.

Keycloak caches themes in memory, so the forced restart is not optional for Keycloak — see Troubleshooting.

What re-running Ansible does to your edits

The role syncs with rsync --ignore-existing, which means:

  • Your edits are preserved. Re-running the role — including a full --tags deploy — will not overwrite a file you changed on the node.
  • But a repo-side change will not reach the node either. Once a file has been seeded, --ignore-existing skips it forever, so fixing a logo in the repository and redeploying does not update a node that already has its own copy. This is the deliberate trade-off: operator edits survive deploys, at the cost of the repo no longer being authoritative for that file.

So if you want a repository change to win on a given node, edit it on the node, or delete the file there and re-run the role so it is re-seeded from the repository:

rm /opt/genieai/config/frontend/genie-ai-config.json
ansible-playbook -i inventory/<env>.ini deploy.yml --tags config --vault-id <env>@prompt

The re-seeded file is live immediately — the bind mount serves it from disk, so a browser reload is all it takes (no frontend restart).

Nodes that you want to follow the repository exactly — CI, or a deployment where nobody edits files by hand — can skip the sync entirely with config_bootstrap_sync_enabled: false.

Frontend brand (logo, favicon, app name, brand color)

The frontend reads its branding from a single JSON file shipped with the build:

  • components/gov-chat-frontend/public/config/genie-ai-config.json

The runtime fetch happens in components/gov-chat-frontend/src/config-loader.js (loadConfig(), run by main.js at startup). The defaults baked into config-loader.js are:

FieldDefaultUsed by
app.titleGENIE.AI<title>, navbar brand text, i18n fallback
app.icon.value/config/logo-genie-ai.jpegNavbar logo + browser favicon (32×32, 16×16, apple-touch)
theme.brandColor#4071cbSets --brand CSS variable on <html>
features.chat.welcomeMessageWelcome to GENIE.AIEmpty-chat greeting
features.chat.botNameGENIE.AIChat subtitle

File locations (frontend)

AssetPath
Runtime JSON configcomponents/gov-chat-frontend/public/config/genie-ai-config.json
Default logo (used if config absent)components/gov-chat-frontend/public/config/logo-genie-ai.jpeg
Brand color → CSS var wiringcomponents/gov-chat-frontend/src/config-loader.js (loadConfig, sets --brand)
Favicon <link> tagscomponents/gov-chat-frontend/public/index.html
Theme tokens consuming --brandcomponents/gov-chat-frontend/public/config/theme-variables.css

How to change the frontend brand

Edit components/gov-chat-frontend/public/config/genie-ai-config.json (or its copy at /opt/genieai/config/frontend/genie-ai-config.json on the node). The file is fetched at runtime from /config/genie-ai-config.json out of the bind-mounted directory, so changing a value needs neither an image rebuild nor a redeploy — just reload the browser, as described in The edit → reload loop.

Example: replace the GENIE.AI brand with a partner institution’s brand:

{
  "app": {
    "title": "MyGov Assistant",
    "icon": {
      "type": "file",
      "value": "/config/logo-mygov.jpeg"
    }
  },
  "theme": {
    "brandColor": "#0a7d52"
  },
  "features": {
    "chat": {
      "welcomeMessage": "Welcome to MyGov Assistant",
      "botName": "MyGov Assistant"
    }
  }
}

Then drop your logo at components/gov-chat-frontend/public/config/logo-mygov.jpeg (same filename as the app.icon.value path, under the public/config/ directory). On a deployed node, drop it at /opt/genieai/config/frontend/logo-mygov.jpeg instead — the bind mount serves it immediately, no container action needed.

The same file also accepts theme.bg, theme.fg, theme.navbar.background, theme.navbar.text, theme.colors.{success,warning,danger,info}, theme.typography.fontFamily, and theme.typography.fontScale. See Theme System → JSON config for the full schema.

How the CSS variable system works

GENIE.AI’s frontend uses a single brand color that drives the entire palette. The flow is:

  1. config-loader.js (run by main.js at startup) reads theme.brandColor from the JSON config and writes it to the <html> element as the CSS custom property --brand (document.documentElement.style.setProperty('--brand', ...)).
  2. theme-variables.css defines --accent, --accent-hover, --accent-muted, --accent-fg, --accent-secondary, --navbar-bg, --navbar-fg — every one derived from --brand using OKLch relative color syntax. The --brand token itself defaults to oklch(47% 0.14 265) (the same hue family as #4071cb) when no config is loaded.
  3. theme-components.css and every DS component reference these tokens via var(--accent), var(--accent-fg), etc. — never hardcoded values.
  4. Dark mode is a separate selector ([data-theme='dark']) in the same files. The same --brand flows through both; only the surface/border/fg tokens flip.

What this means in practice: you only set one color, and the entire UI follows. Hover, pressed, focus, navbar, buttons, links, charts all derive automatically. No JavaScript color math, no per-component configuration.

For the complete token reference (colors, spacing, typography, radii, shadows) and how to override tokens per-component without !important, see:

The default logo path is /config/logo-genie-ai.jpeg (referenced in config-loader.js as a bundled default if genie-ai-config.json cannot be fetched). To use your own logo:

  1. Drop the file into components/gov-chat-frontend/public/config/ using a descriptive filename:
    • components/gov-chat-frontend/public/config/logo-<your-org>.jpeg (or .png/.svg)
  2. Update genie-ai-config.json to point app.icon.value at the new path:
    "icon": {
      "type": "file",
      "value": "/config/logo-<your-org>.jpeg"
    }
    
  3. Pick up the new file — the config/ directory is bind-mounted, so the file is live immediately; no rebuild and no container restart. A brand-new filename is the one case where a normal browser reload is not enough: the old 404 was cached, so hard-reload once.

Recommended image specs:

  • Format: .png or .svg for crispness on retina displays; .jpeg is acceptable for photographic logos.
  • Dimensions: at least 256×256 px so the navbar version (downscaled to ~32 px tall) and the favicon (32×32 and 16×16) both render cleanly.
  • Background: prefer a transparent background (.png/.svg) so the logo sits cleanly on both light and dark navbar variants.

The same app.icon.value path is used for three <link rel="icon"> tags (32×32, 16×16, apple-touch 180×180), so a single square logo covers all three sizes. The browser downsamples; no per-size asset is required.

Keycloak login screen branding

The Keycloak login, account, admin, and email screens are themed by the genie theme under configs/keycloak/themes/genie/. Each surface has its own theme.properties and its own resources/ directory:

Surfacetheme.propertiesLogo locationFaviconStylesheet
Loginconfigs/keycloak/themes/genie/login/theme.propertiesresources/img/logo.pngresources/img/favicon.icoresources/css/genie.css, resources/css/dark.css
Accountconfigs/keycloak/themes/genie/account/theme.propertiesresources/img/logo.png(default favicon)resources/css/genie.css, resources/css/dark.css
Adminconfigs/keycloak/themes/genie/admin/theme.propertiesresources/img/logo.pngresources/img/favicon.icoresources/css/genie.css, resources/css/dark.css
Emailconfigs/keycloak/themes/genie/email/theme.propertiesresources/img/logo.pngn/an/a

The genie theme is a child of the Keycloak keycloak.v2 (login/admin) / keycloak.v3 (account) / keycloak (email) base themes — defined by the parent= line in each theme.properties.

Multiple themes and theme selection

The runtime mount delivers the whole themes/ directory to Keycloak (/opt/keycloak/themes), not a single theme. This matters in two ways:

  • Extra themes work. You can ship additional themes next to genie — for example genie-el-salvador/, whose theme.properties declares parent=genie. Such a parent reference only resolves when both directory names exist under the mounted directory. (Mounting one theme under a fixed name would make it inherit from itself and hang Keycloak’s theme resolver — which is why the mount is the themes root.)
  • The active theme is a setting, not the mount. All four surfaces follow the KEYCLOAK_THEME environment variable at realm-import time (configs/keycloak/genie-realm.yaml). Default: genie. To activate another theme:
# Either redeploy with the variable set (realm import applies it):
#   .env: KEYCLOAK_THEME=genie-el-salvador   (Ansible: keycloak_theme group var)
# Or switch a running realm via the admin API:
ADMIN_TOKEN=$(curl -sk -X POST "https://<host>/auth/realms/master/protocol/openid-connect/token" \
  -d "client_id=admin-cli" -d "username=admin" -d "password=<KEYCLOAK_ADMIN_PASSWORD>" -d "grant_type=password" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
curl -sk -X PUT "https://<host>/auth/admin/realms/genie" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"loginTheme":"genie-el-salvador"}'

Then restart the Keycloak container — it caches themes in memory.

Adding a new theme in three steps: (1) create configs/keycloak/themes/<your-theme>/ with login/ (and optionally account/, admin/, email/) subdirectories, each with a theme.properties — parent=genie reuses the GENIE.AI layout and only overrides your files; (2) deploy — the config_bootstrap role seeds the whole themes/ tree; (3) set KEYCLOAK_THEME (or the realm setting) to <your-theme>. On an already-deployed node, dropping the theme directory into /opt/genieai/config/keycloak/themes/ and restarting Keycloak is enough — the bind mount serves it live.

KEYCLOAK_THEME applies to all four surfaces at once (login, account, admin, email — see genie-realm.yaml). A theme that only ships login/ therefore leaves the account console, the admin console and emails on Keycloak’s unbranded built-in look. If that is not what you want, either ship the other subdirectories too, or set the per-type realm settings (accountTheme, adminTheme, emailTheme) to genie via the admin API while pointing loginTheme at your theme.

Replace four PNG files (one per surface) and (optionally) tweak the CSS:

  1. Drop your logo at the same path in each of the four surfaces:
    • configs/keycloak/themes/genie/login/resources/img/logo.png
    • configs/keycloak/themes/genie/account/resources/img/logo.png
    • configs/keycloak/themes/genie/admin/resources/img/logo.png
    • configs/keycloak/themes/genie/email/resources/img/logo.png
  2. Keep the filename logo.png — every template references it directly:
    • configs/keycloak/themes/genie/login/template.ftl: <img src="${url.resourcesPath}/img/logo.png" alt="GENIE.AI" ...>
  3. Replace the alt text in each template.ftl if your brand name changed (search for alt="GENIE.AI").
  4. (Optional) Tweak the brand color in configs/keycloak/themes/genie/{login,account,admin}/resources/css/genie.css — the admin theme especially uses several brand-tinted backgrounds.
  5. Force a Keycloak container restart: docker service update --force <stack>_keycloak (Compose: docker compose up -d --force-recreate keycloak). Keycloak caches themes in memory; always restart the Keycloak container after a theme file change — see Troubleshooting.

Logo image specs: Keycloak renders the login logo at width: 120px (height auto, scales with aspect ratio) per configs/keycloak/themes/genie/login/resources/css/genie.css. Use a transparent PNG at least 240px wide (2× for retina) so it stays sharp on high-DPI screens. The admin logo is constrained to height: 32px in configs/keycloak/themes/genie/admin/resources/css/genie.css. For the email surface, the logo is embedded as a CID attachment and capped at max-width:120px; max-height:40px in template.ftl — keep the file’s natural dimensions in that range to avoid downscaling artifacts.

Keycloak email branding

GENIE.AI-themed Keycloak emails (verification, password reset, etc.) use a single shared FreeMarker layout:

  • configs/keycloak/themes/genie/email/html/template.ftl

The logo is embedded via a CID attachment (<img src="cid:logo.png" alt="GENIE.AI"> on line 17), with the image file at configs/keycloak/themes/genie/email/resources/img/logo.png. Keycloak auto-attaches any file under resources/img/ as a CID; do not change the src="cid:logo.png" reference unless you also rename the file.

To rebrand the emails:

  1. Replace configs/keycloak/themes/genie/email/resources/img/logo.png with your logo (keep the filename).
  2. Edit the two visible strings in template.ftl:
    • Line 8: <title>${subject!"GENIE.AI"}</title> — the email’s <title> fallback (used by the subject only when no subject is set; most clients show the actual subject).
    • Line 18: <div ...>GENIE.AI</div> — the visible brand text rendered next to the logo.
  3. The footer at lines 27–30 hardcodes “International Telecommunication Union (ITU)” and the Geneva address. Replace with your institution’s footer text if rebranding.
  4. The header gradient (line 16) uses a hardcoded ITU-blue: background:linear-gradient(135deg,#2b4acb 0%,#1e3a8a 100%);. Replace with your brand colors.

Restart Keycloak after the change (see Troubleshooting).

Favicon

The Keycloak login and admin surfaces each ship their own favicon file:

  • configs/keycloak/themes/genie/login/resources/img/favicon.ico
  • configs/keycloak/themes/genie/admin/resources/img/favicon.ico

Replace each .ico with your institution’s favicon. Use the same file in both locations — they are served independently by Keycloak per theme. For the account surface, no explicit favicon is configured; Keycloak falls back to the realm default.

The web app’s favicon is controlled separately (see Per-institution logo) via the <link rel="icon"> tags in components/gov-chat-frontend/public/index.html, which are re-pointed at runtime by config-loader.js (loadConfig()) from app.icon.value.

Verify it worked

After a redeploy, verify each surface end-to-end before announcing the rebrand:

Frontend brand

  1. Open the deployed web app in a browser (hard-refresh: Ctrl+Shift+R / Cmd+Shift+R to bypass cache).
  2. Brand color: open DevTools → Elements → click the <html> element → look at the Computed pane for --brand. The value should match your theme.brandColor.
    // In the browser console:
    getComputedStyle(document.documentElement).getPropertyValue('--brand').trim()
    // → "#0a7d52"
    
  3. Logo: open DevTools → Network tab → filter by logo → reload the page → verify your logo URL (e.g. /config/logo-mygov.jpeg) returns 200 OK and your image is loaded. The navbar should display it.
  4. Favicon: DevTools → Network tab → filter by favicon → the three link[rel=icon] requests should return your logo URL.
  5. App title: hover the browser tab — the <title> should match app.title. Also check the navbar brand text.
  6. Dark mode: toggle dark mode in the UI → verify the navbar background, accent buttons, and links all derive from your brand color. The --brand value does not change between modes; the surrounding tokens do.

Keycloak login

  1. Sign out (or open an incognito window) and navigate to the login page.
  2. Logo: confirm your logo renders in the header.
  3. Favicon: confirm your favicon appears in the browser tab.
  4. Theme colors: confirm the brand-tinted accents (button background, focus rings) reflect your CSS changes — assuming you edited the CSS.
  5. Email: trigger a verification or password-reset email → confirm the embedded logo and footer text render correctly. (Most clients cache email images; test from a fresh email address.)

Common verification gotchas

  • Stale browser cache: /config/ and /runtime/config.js are served Cache-Control: no-cache, must-revalidate, so a normal reload picks up your change — you do not need to configure anything on the proxy. The one exception is the first reload after upgrading from a deployment that predates this change: those paths used to be served a one-year immutable cache, and a browser that fetched them then will reuse the stored copy until you hard-refresh once.
  • Keycloak theme cache: Keycloak caches themes in its infinispan layer at startup. A theme file change requires a Keycloak restart, not just a deploy. A redeploy that does not restart Keycloak will keep the old theme in memory.
  • Two favicon files: the web app’s favicon and the Keycloak login’s favicon are served from different origins and cached independently. Test each one separately.

Troubleshooting

Logo doesn’t show on the web app

  • Wrong path. app.icon.value must point at a file under public/config/ (served at /config/). The leading slash matters. If you forget it, the request becomes relative to the current page and 404s on most routes.
  • Wrong filename. The path in the JSON must match the actual filename on disk, including the extension. The frontend does no extension rewriting.
  • The mount is not wired. A directory bind mount is live: a file edited on the node is visible in the container immediately, no restart needed. If docker exec <frontend-container> ls /app/dist/config/ does not list your file, the container is not mounting the directory you edited — check CONFIG_DIR_FRONTEND in the node’s env file, not the container’s age.
  • Cached file. Hard-refresh. If the problem persists, check docker exec <frontend-container> ls /app/dist/config/ — your new file should be there.

Theme overrides ignored after Keycloak restart

  • Wrong parent= line. The parent= in each theme.properties must match a theme Keycloak ships — keycloak.v2 (login/admin), keycloak.v3 (account), keycloak (email). A typo silently fails to inherit the parent styles, leaving the theme unstyled.
  • Theme not registered. Keycloak must be told the theme exists. In GENIE.AI deployments this is handled by the realm-import JSON under configs/keycloak/; verify the theme is listed. See Keycloak Admin Guide → Theming for the registration flow.
  • CSS specificity. Keycloak’s base keycloak.v2 styles are aggressive. Use higher-specificity selectors or !important (Keycloak-acceptable in theme CSS, unlike the web app where !important is forbidden).
  • Stale infinispan cache. After a theme file change, restart Keycloak twice if the first restart still shows old styles — infinispan can survive a single restart in some versions.

CSS not applying (frontend)

  • !important rules in your code. The Design System forbids !important in component styles. If you find yourself reaching for it, you’re probably overriding the wrong token. See Theme System → Switching themes for the CSS-variable-based override pattern.
  • Token name typo. var(--accent) works; var(--acccent) silently fails and the property becomes its inherited value (or unset). DevTools → Computed shows the resolved value; check the source line if it’s not what you expect.
  • Config not loading. Open DevTools → Network → look for a request to /config/genie-ai-config.json. A 404 or 500 means the file is missing or malformed; the app falls back to the defaults baked into config-loader.js.
  • Hardcoded hex in a component. New components must reference DS tokens (var(--accent), var(--fg), etc.). Hardcoded values bypass the theme entirely. Use Grep CSS to audit: grep -rn '#[0-9a-fA-F]\{3,8\}' components/gov-chat-frontend/src/components/ — anything that is not a comment or a test fixture is a violation.

Dark mode tokens don’t change with the brand color

This is expected: --brand is the same in light and dark mode; only the surrounding tokens (--bg, --surface, --fg, --border, --muted) flip. If your brand color looks wrong in dark mode, the issue is contrast — --navbar-fg and --accent-fg are derived from --brand using OKLch lightness shifts, so they should remain readable. If they don’t, you’ve overridden one of the derived tokens with a hardcoded value.