Skip to content

Branding

The brand block in nimblebrain.json white-labels a deployment: your users see your product name, your logos, your accent color and your typefaces in place of NimbleBrain’s. Every key is optional. Without a brand block the deployment is NimbleBrain, unchanged.

This page is the reference for every key. To brand a deployment step by step (preparing assets, choosing colors that pass, and checking the result), follow White-labeling.

brand is a top-level key of nimblebrain.json, not a preference, and it is not read from nimblebrain.overrides.json. Changing it needs a restart.

Surface Reads
The agent’s identity line (“You are a helpful assistant powered by …”) name
OAuth consent screens at connector vendors (dynamic client registration) name, homepageUrl, logo.raster
The OAuth “You’re in.” success page and the OAuth error pages name, colors
Connector-tool error messages name
The logo on the sign-in page and in the expanded sidebar logo.light, logo.dark
The logo in the collapsed sidebar logo.mark
The browser tab title name
The favicon favicon
The shell’s accent, focus ring, neutrals, sidebar, dark-mode surfaces and typefaces colors, fonts
Embedded apps’ accent, focus ring and typefaces (the theme tokens the shell injects) colors, fonts
The “Leaving …” dialog shown before an external link opens name
The notification route help text in workspace settings name
The sidebar’s home link, as a screen reader names it name
The color theme a person starts in defaultTheme
GET /v1/brand.js, which the web client loads before its bundle the whole block

A brand that sets name but no logo or favicon keeps the bundled NimbleBrain artwork in those places, with your name as the logo’s alt text, so set them together.

Wire identifiers do not change: MCP client and server names, the nb__ tool namespace, ai.nimblebrain/* metadata keys, the NB_ environment prefix, the nimblebrain.json filename. They identify the software to other software, not to a person.

Key Type Description
name string Product name shown wherever a person reads “NimbleBrain”. 1–64 characters, no <, >, {, }. Default: "NimbleBrain".
homepageUrl string Your homepage. Sent as client_uri when registering OAuth clients.
logo.light string Full logo for light mode.
logo.dark string Full logo for dark mode.
logo.mark string Square mark, for places the full logo does not fit.
logo.raster string PNG mark, sent as logo_uri when registering OAuth clients. A raster because several identity providers refuse to render SVG there.
favicon string Favicon URL.
defaultTheme "light" | "dark" | "system" The color theme a person starts in until they choose one. "system" follows their operating system. A person’s own choice always wins. Default: "system".
colors object Color roles: primary, ring, highlight, sidebar. See Colors.
fonts object Typeface overrides for the sans, heading and reading roles. See Fonts.

Every URL is absolute (http:// or https://) and must resolve. A set URL that returns 404 renders a broken image, not the default artwork, so publish assets before you reference them.

When name is set, OAuth registration sends only your values: without a homepageUrl no client_uri is sent, and without a logo.raster no logo_uri is sent. NimbleBrain’s are never sent on your behalf.

colors sets up to four roles, each a [light, dark] pair of #rrggbb hex colors, the same shape the palette itself uses. A role is not a single token: each one derives a family of tokens, so you name the few colors your identity is made of and the shell stays coherent around them. Set only primary for a light touch; add the others for more of your brand.

Role What it derives
primary The accent: primary buttons, links, accent text. Its hue also tints the neutrals (text, borders, gray surfaces) at their canonical lightness, so the whole shell takes your color’s cast without any contrast moving.
ring The focus ring. Usually the same value as primary.
highlight The fill of the active sidebar row. Text on it is white or black, whichever reads better, so a bright color such as a yellow works. A dark light-mode sidebar renders under the dark palette, so its row shows the dark value in both modes and the light value is unused.
sidebar Two jobs. The light value is the light-mode sidebar ground: a light color tints it; a dark color turns the sidebar into a dark panel, rendered under the dark palette, with the dark logo. The dark value anchors dark mode: it is the deepest surface, and the page, cards, muted fills and borders are lighter steps of the same hue, so dark mode reads as your color rather than black.

Without sidebar, dark mode keeps the canonical near-black, tinted by primary.

Some colors follow the roles and are not set. The text on a primary fill is, per mode, white or black, whichever has the higher contrast. The ground of info notices is a tint of your primary over the page background: 10% in light mode, 20% in dark. On a dark ground derived from sidebar, the canonical status colors (error, success, warning, in-progress) and chart colors lighten just enough to stay legible. Your own colors are never adjusted: one that fails is rejected.

The runtime merges your colors over the canonical palette at startup and checks the result against every contrast pair the canonical palette is held to, in both modes: the derived text on your primary fill, accent text on the page and on cards, the accent’s hover fill, and the rest. Every pair must clear 4.5:1, the WCAG 2.2 AA minimum for body text.

A brand that fails any pair stops the runtime at startup with an error naming the pair, the mode, the two colors and the ratio:

Invalid brand in config: light mode: primary on background (links, accent text) is 1.61:1 (#CCCCCC on #ffffff), below the 4.5:1 WCAG AA minimum

A primary that fails is too light for light mode or too dark for dark mode. Darken the light value, or lighten the dark one, until it passes. With a sidebar set, the dark value must read on the dark ground that sidebar derives, which is lighter than black: lighten it a little more.

Each role takes a CSS stack (required) and, optionally, one file to load:

Key Description
stack The CSS font-family value, fallbacks included: "'Instrument Sans', system-ui, sans-serif".
family The family name the file is declared under — the name stack refers to it by. Required with url.
url One woff2 file, typically a variable font covering every weight you use.
weight The font-weight that file covers: "400", or a variable range "400 700". Only with url.

The roles are sans (interface text), heading and reading (the agent’s prose in chat). Code keeps the canonical monospace face.

A font URL is a .woff2 file, never a stylesheet. Google Fonts CSS URLs (https://fonts.googleapis.com/css2?…) are rejected. Embedded apps load faces from the file URL directly, so a stylesheet that points at files cannot reach them. Self-host the woff2 files, and make sure their license allows redistribution.

The server hosting the font files must send Access-Control-Allow-Origin: *. Browsers fetch fonts in CORS mode, and an embedded app runs in an opaque origin, so its requests arrive with Origin: null and no narrower value matches them. Without the header the shell may still render the font while apps silently fall back to the next family in stack.

A role with no url must name a font your users already have — a system font such as Georgia. Nothing is downloaded for it.

A complete block for a fictional ACME deployment:

nimblebrain.json
{
"brand": {
"name": "ACME",
"homepageUrl": "https://acme.example",
"logo": {
"light": "https://static.example.com/brands/acme/logo-light.svg",
"dark": "https://static.example.com/brands/acme/logo-dark.svg",
"mark": "https://static.example.com/brands/acme/mark.svg",
"raster": "https://static.example.com/brands/acme/mark-128.png"
},
"favicon": "https://static.example.com/brands/acme/favicon.png",
"defaultTheme": "light",
"colors": {
"primary": ["#B53707", "#FF8A4C"],
"ring": ["#B53707", "#FF8A4C"],
"highlight": ["#FFC94A", "#FFC94A"],
"sidebar": ["#2A1710", "#1C0F0A"]
},
"fonts": {
"heading": {
"stack": "'Syne', system-ui, sans-serif",
"family": "Syne",
"url": "https://static.example.com/brands/acme/fonts/syne-latin-wght-normal.woff2",
"weight": "400 800"
},
"sans": {
"stack": "'Instrument Sans', system-ui, sans-serif",
"family": "Instrument Sans",
"url": "https://static.example.com/brands/acme/fonts/instrument-sans-latin-wght-normal.woff2",
"weight": "400 700"
},
"reading": {
"stack": "'Fraunces', Georgia, serif",
"family": "Fraunces",
"url": "https://static.example.com/brands/acme/fonts/fraunces-latin-wght-normal.woff2",
"weight": "300 700"
}
}
}
}

The runtime serves the validated block as a script at GET /v1/brand.js, unauthenticated, with Content-Type: text/javascript and Cache-Control: public, max-age=60. The body is one assignment:

window.__NB_BRAND__ = {"name":"ACME","defaultTheme":"light"};

The web client loads it with a plain <script> before its bundle, so the brand is in place before the first paint. It is public because the sign-in page shows the brand before anyone has signed in, and nothing in it is private. Without a brand block it assigns {}.

The served block is the configured one, holding only the keys on this page. A key the schema does not define is reported as an unknown key at startup and ignored; it is never served.

A change reaches a visitor on their next page load once that minute of cache has passed. Removing the brand block restores the default look the same way.