Branding Tokens

How to read the tenant's branding from GraphQL (or the REST splash endpoint), what the token model actually looks like, and how to apply it in your own surface.

Last updated May 19, 2026

Tenant branding is exposed two ways: a structured GraphQL query (branding) and a flat REST endpoint (GET /api/branding) the proxy uses to render an HTML splash before the JS engine boots. This page covers both and walks through the actual shape of the data — preset themes, generated palettes, and an SVG-keyed LogoSet — not the simplified "primary + hover + active" model an earlier draft of this article invented.

Two endpoints, two purposes

EndpointWhen to use itAuthSource
query branding (GraphQL)Full tree — every color, logo, every per-app override. Needed by client apps that render the tenant's UI.Any caller in the tenant's context (JWT, btk_, or bpk_).branding: BrandingConfig on Queryschema.graphql:27629
GET /api/branding (REST)Flat payload for HTML <head> injection — splash color, OG/Twitter meta, favicon, public client key.None — tenant is resolved from Host.unified_schema/branding_api.rs:155

If you're inside an authenticated SDK, use the GraphQL query. If you're a reverse proxy stamping <meta> tags into an HTML shell, use the REST endpoint.

The GraphQL shape

The schema models branding as BrandingConfig (schema.graphql:4361) with three levels — mirroring how operators edit it:

  • company: CompanyBrand! — company name, tagline, social links, legal URLs, a fallback LogoSet.
  • verticals: [VerticalBrandEntry!]! — one entry per vertical the tenant enabled (TAXI, SHOP, PARKING, SERVICE). Each holds a theme choice, resolved palette, typography, corner radius, and per-app overrides.
  • version: Int! — monotonic, bumped whenever branding is published.
  • createdAt: DateTime! — when this revision was created.

A minimal query the website itself runs (full version: website/lib/graphql/documents/branding.graphql):

query Branding {
  branding {
    version
    company {
      companyName
      tagline
      logo { primary { id downloadUrl } iconDark { id downloadUrl } }
      privacyPolicyUrl
      termsUrl
    }
    verticals {
      vertical
      brand {
        theme { preset custom { primary secondary tertiary } }
        colors { ...FullColorPalette }
        lightPalette { ...FullColorPalette }
        darkPalette { ...FullColorPalette }
        typography { headingFont bodyFont }
        radiusTheme
        apps {
          app
          brand {
            name
            tagline
            logo { primary { id downloadUrl } }
            supportEmail
          }
        }
      }
    }
  }
}

The theme model

ThemeChoice (schema.graphql:36737) is either a preset or a custom seed:

type ThemeChoice {
  preset: BrandTheme   # one of nine
  custom: CustomSeed   # primary + optional secondary + tertiary
}

The nine presets (schema.graphql:4322):

Enum valueNotes
COBALTDefault — professional blue
CORAL_REDWarm, energetic
EARTHY_GREENEco / wellness
SUNBURST_YELLOWBright
HYPER_PINKBold, modern
ELECTRIC_INDIGOTech-forward
AUTUMN_ORANGEFriendly
NOIRMinimal dark
OCEAN_TEALCalm

There are not three theme tokens (primary / primaryHover / primaryActive). The full palette is ColorPalette (schema.graphql:5947) — a Material 3-style set with primary, primaryBold, primaryDisabled, primaryVariant, primaryVariantLow, onPrimary, primaryContainer, and the parallel secondary / tertiary / surface / outline / success / warning / error / info / insight families, plus fixed fixedLight / fixedDark for things that should never invert. The palette is generated from the preset or seed and stored on the vertical brand alongside the theme choice.

Corner radius

radiusTheme: RadiusTheme! (schema.graphql:29636) — one of:

  • ROUNDED — default, 8 / 10 / 12 px
  • SHARP — 2 / 4 / 6 px, geometric
  • PILL — 16 / 20 / 24 px, playful

Apply to your buttons / cards / chips at whichever scale you've named your radius tokens.

Typography

Typography { headingFont, bodyFont } (schema.graphql:37650). Both are nullable strings — the Google Fonts family name (e.g. "Poppins", "Inter"). The Flutter design system loads them at runtime via google_fonts; for web, treat them as the font-family value and load the matching @import from fonts.googleapis.com.

Logos

LogoSet (schema.graphql:16399) has nine slots, all but primary nullable:

SlotUse
primaryRequired. The default logo.
primaryDarkVariant for dark backgrounds.
icon / iconDarkSquare icon (app icon, favicon).
monochromeSingle-color version for stamping.
wordmarkHorizontal / wordmarkHorizontalDarkLandscape text logo.
wordmarkVertical / wordmarkVerticalDarkStacked/portrait text logo.

Each slot is an Upload! — query { id, downloadUrl } to get a stable CDN URL.

Logos resolve in a fallback chain: per-app LogoSet → per-vertical → company.logo. Most consumer apps use wordmarkVerticalDark on their splash and fall back to primary / wordmarkVertical (see branding_api.rs:443-447).

The REST splash endpoint

GET /api/branding
Host: rides.acme.com

Returns a flat JSON payload (branding_api.rs:47BrandingResponse):

{
  "appName": "AcmeRides",
  "companyName": "Acme Transportation Inc.",
  "tagline": "Get there faster",
  "publicClientKey": "bpk_acme_abc123…",
  "app": "TAXI_PASSENGER",
  "primaryColor": "#0253E8",
  "onPrimaryColor": "#FFFFFF",
  "primaryColorDark": "#5B8DEF",
  "onPrimaryColorDark": "#000000",
  "faviconUrl": "https://.../favicon.png",
  "splashLogoUrl": "https://.../splash.png",
  "splashLogoDarkUrl": "https://.../splash_dark.png",
  "ogUrl": "https://rides.acme.com/",
  "ogSiteName": "Acme Transportation Inc.",
  "ogLocale": "en_US",
  "ogType": "website",
  "ogImage": "https://.../splash.png",
  "ogImageWidth": 1200,
  "ogImageHeight": 630,
  "twitterCard": "summary_large_image",
  "twitterSite": "@acme",
  "twitterCreator": "@acme",
  "twitterImageAlt": "AcmeRides — Get there faster",
  "twitterImage": "https://.../splash.png",
  "twitterImageWidth": 1200,
  "twitterImageHeight": 630,
  "googleMapsApiKey": "AIza…"
}

The endpoint resolves the tenant from Host, looks up the matching tenant_domains row, and chooses the active app from it. If the host doesn't resolve, it returns a default-branding payload with the host echoed back and cache-control: no-cache so nothing gets stuck. Successful responses ship with:

cache-control: public, max-age=300, stale-while-revalidate=60

(branding_api.rs:380). 5 minutes fresh, 1 minute stale-while-revalidate — not the 5-min / 1-hour split an earlier version of this page advertised.

There is no brandingUpdates subscription. The Root Subscription type has nothing for branding (schema.graphql:34596 lists driverEvents, passengerEvents, shopCustomerEvents, vendorEvents, parkingUserEvents, adminEvents — no branding). If you need to detect a change, re-query branding { version } on a schedule (cheap — the response is small and cache-friendly) and compare versions.

Version & rollback

BrandingConfig.version bumps every time an operator publishes a change via updateBranding, updateCompanyBrand, setVerticalBrand, or setVerticalAppBrand — all of which return the new BrandingConfig. Revisions are kept; you can pull a specific one with brandingVersion(version: Int!) or list the history with brandingRevisions: [BrandingConfig!]!. The rollbackBranding(version: Int!) mutation re-applies an old revision as the new current.

Applying in web

Pull once, write to CSS variables on the document root, then style by variable:

const { branding } = await getBranding(token);
const v = branding.verticals.find(e => e.vertical === "TAXI")?.brand;
if (v) {
  const root = document.documentElement;
  root.style.setProperty("--color-primary", v.colors.primary);
  root.style.setProperty("--color-on-primary", v.colors.onPrimary);
  root.style.setProperty("--color-surface", v.colors.surface);
  root.style.setProperty("--font-heading", v.typography.headingFont ?? "Inter");
  root.style.setProperty("--font-body", v.typography.bodyFont ?? "Inter");
}

Applying in Flutter

The internal Flutter SDK loads typography via google_fonts and ships the palette to the better_design_system ThemeData extension automatically — feature code reads tokens like context.colors.primary without touching the GraphQL response directly. If you're embedding inside a third-party Flutter app, mirror the same: pull the vertical brand for the app you're rendering, build a ColorScheme from light / darkPalette, and pass it through your MaterialApp.

Multi-brand: a clarifying note

Operators can configure per-vertical branding (every vertical has its own theme, typography, radius) and per-app overrides (apps: [VerticalAppBrandEntry!]! inside each vertical, overriding name, tagline, logo, support email). There is no separate "brand" entity the previous version of this page invented (brand(id: BrandId!)). All branding hangs off the single BrandingConfig returned by the branding query, scoped to the current tenant.

If two tenants need entirely separate brands, that's two tenants — not two brands on one tenant.

What's next

  • Operator-facing Branding guide — what the dashboard looks like, the preset table, the per-vertical/per-app form.
  • Custom Domains/api/branding resolves through the same Hosttenant_domains chain.
  • Quickstart — the smallest possible GraphQL call to confirm your key works.

Build the foundation once. Expand without limits.

BetterSuite is built for teams who see on-demand as a business — not a feature.