If you are building a trading platform for multiple clients - brokers, exchanges, or liquidity providers - you will eventually need to white-label it. Each client wants their logo, their colours, and ideally their custom domain. The technical challenge is supporting this without maintaining separate codebases.
What white-labelling actually requires
At minimum, clients expect to customise:
- Logo and favicon - Their brand identity in the header and browser tab
- Colour scheme - Primary brand colour applied throughout the interface
- Typography - Some clients have brand fonts
- Domain - Trading on
trade.clientbrand.com, not your domain - Email templates - Notifications that come from their brand
More demanding clients also want:
- Custom landing pages - Different marketing content per client
- Feature toggles - Different modules enabled per client
- Custom terminology - "Wallet" vs "Account", "Spot" vs "Cash"
The architecture
Tenant configuration
Define a tenant configuration type that captures everything that varies between clients:
interface TenantConfig { id: string; name: string; domain: string; theme: { primaryColor: string; primaryForeground: string; logoUrl: string; faviconUrl: string; fontFamily?: string; }; features: { spot: boolean; futures: boolean; margin: boolean; staking: boolean; }; terminology: Record<string, string>; }
Load this configuration at application startup based on the domain or a subdomain identifier.
CSS custom properties as the theming layer
CSS custom properties are the correct abstraction for white-labelling. They cascade through the DOM, can be set at runtime, and require zero JavaScript to apply:
function applyTenantTheme(config: TenantConfig) { const root = document.documentElement; root.style.setProperty("--color-primary", config.theme.primaryColor); root.style.setProperty( "--color-primary-foreground", config.theme.primaryForeground ); if (config.theme.fontFamily) { root.style.setProperty("--font-sans", config.theme.fontFamily); } }
Your components reference these variables through Tailwind or direct CSS. When the variables change, the entire UI updates without re-rendering a single React component.
Hedge UI's theme layer treats every brand-customisable surface as a CSS variable. Beyond the standard shadcn tokens (--background, --foreground, --primary), we add trading-specific semantic tokens - --bid, --ask, --buy, --sell - so a client who wants their bid colour to be blue instead of green overrides one variable and every order book row, depth bar, and chart overlay picks up the new value. The same set is redefined under the .dark class for dark mode, because the colours that look right on white are too saturated against near-black.
Isolating brand assets
Store tenant assets (logos, favicons, custom images) in a structured directory or CDN path:
/tenants /client-a logo.svg favicon.ico og-image.png /client-b logo.svg favicon.ico og-image.png
Reference these through the tenant configuration rather than hardcoding paths.
Feature toggling
Not every client gets every feature. Use the tenant configuration to conditionally render modules:
function TradingLayout({ config }: { config: TenantConfig }) { return ( <Workspace> <ChartPanel /> <OrderBookPanel /> {config.features.futures && <FuturesPanel />} {config.features.margin && <MarginPanel />} {config.features.staking && <StakingPanel />} </Workspace> ); }
For more granular control, use a feature flag service that can be updated without deployments.
Custom terminology
Some clients call the same concept by different names. A translation layer keeps this out of your component logic:
function useTerm(key: string): string { const config = useTenantConfig(); return config.terminology[key] ?? key; } // In a component const walletLabel = useTerm("Account"); // Returns "Wallet" for Client A
Keep the default terminology in the components and override only where the client differs.
Multi-domain deployment
Each client wants their own domain. In Next.js, handle this with middleware:
// middleware.ts export function middleware(request: NextRequest) { const hostname = request.headers.get("host") ?? ""; const tenant = getTenantByDomain(hostname); if (tenant) { request.headers.set("x-tenant-id", tenant.id); } return NextResponse.next({ request }); }
The tenant ID propagates through the request, and server components can load the correct configuration.
Testing white-label configurations
Every tenant configuration is a potential source of bugs. Test each one:
- Visual regression tests - Capture screenshots of key pages with each tenant's theme applied. Compare against baselines.
- Feature matrix tests - Verify that enabled/disabled features render correctly per tenant.
- Theme contrast checks - Ensure that each client's primary colour meets contrast requirements against your surface colours.
Automate these in CI with a matrix build that iterates over tenant configurations.
Common mistakes
- Forking the codebase per client - This scales to about 3 clients before becoming unmaintainable. Use configuration, not branches.
- Hardcoding colours in components - Every colour reference must go through the token system. A single hardcoded
#3b82f6will look wrong in every non-default theme. - Client-specific logic in core components - If you find yourself writing
if (tenant === "clientA"), extract the varying behaviour into the configuration. - Forgetting email templates - Users receive emails from the platform. These need to match the client's branding too.
Starting from the right foundation
White-labelling is dramatically easier when the application is built with theming from day one. Retrofitting CSS custom properties onto a codebase with hardcoded colours and inline styles is painful and error-prone.
If you know your platform will serve multiple clients, invest in the token-based theming system early. The marginal cost of making a component theme-aware during development is far lower than refactoring it later.
