White Labeling: Customize App Per Client
Learn: White Labeling: Customize App Per Client
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
White Labeling: Customize App Per Client - Theming and Branding
Problem
Building a single application that serves multiple clients while maintaining distinct brand identities is challenging. Each client needs their own colors, logos, fonts, and UI elements without deploying separate codebases. Managing these variations across components, maintaining consistency, and scaling to hundreds of clients becomes complex without a proper white-labeling architecture.
Solution
Implement a multi-tenant white-labeling system using:
- Dynamic theme configuration loaded per client
- CSS-in-JS or CSS variables for runtime theming
- Asset CDN for client-specific logos and images
- Configuration service that determines client context
- Component library that respects theme tokens
This approach allows one codebase to serve unlimited branded instances while keeping deployment and maintenance simple.
Code Implementation
1. Theme Configuration Structure
// types/theme.ts
export interface ThemeConfig {
clientId: string;
name: string;
colors: {
primary: string;
secondary: string;
accent: string;
background: string;
text: string;
border: string;
success: string;
error: string;
warning: string;
};
typography: {
fontFamily: string;
headingFont: string;
baseFontSize: number;
};
branding: {
logoUrl: string;
faviconUrl: string;
companyName: string;
};
spacing: {
unit: number;
};
}
export const DEFAULT_THEME: ThemeConfig = {
clientId: 'default',
name: 'Default Theme',
colors: {
primary: '#007AFF',
secondary: '#5AC8FA',
accent: '#FF2D55',
background: '#FFFFFF',
text: '#000000',
border: '#E5E5EA',
success: '#34C759',
error: '#FF3B30',
warning: '#FF9500',
},
typography: {
fontFamily: 'Inter, sans-serif',
headingFont: 'Poppins, sans-serif',
baseFontSize: 16,
},
branding: {
logoUrl: 'https://cdn.example.com/default-logo.png',
faviconUrl: 'https://cdn.example.com/default-favicon.ico',
companyName: 'Default Company',
},
spacing: {
unit: 8,
},
};
2. Theme Service
// services/themeService.ts
import { ThemeConfig, DEFAULT_THEME } from '../types/theme';
class ThemeService {
private currentTheme: ThemeConfig = DEFAULT_THEME;
private themeCache: Map<string, ThemeConfig> = new Map();
private listeners: Set<(theme: ThemeConfig) => void> = new Set();
async loadTheme(clientId: string): Promise<ThemeConfig> {
// Check cache first
if (this.themeCache.has(clientId)) {
const cachedTheme = this.themeCache.get(clientId)!;
this.setCurrentTheme(cachedTheme);
return cachedTheme;
}
try {
// Fetch from API or config server
const response = await fetch(
`${process.env.REACT_APP_CONFIG_API}/themes/${clientId}`
);
if (!response.ok) {
console.warn(`Theme not found for ${clientId}, using default`);
return DEFAULT_THEME;
}
const theme: ThemeConfig = await response.json();
this.themeCache.set(clientId, theme);
this.setCurrentTheme(theme);
return theme;
} catch (error) {
console.error('Failed to load theme:', error);
return DEFAULT_THEME;
}
}
private setCurrentTheme(theme: ThemeConfig): void {
this.currentTheme = theme;
this.applyThemeToDOM(theme);
this.notifyListeners(theme);
}
private applyThemeToDOM(theme: ThemeConfig): void {
const root = document.documentElement;
// Set CSS variables
root.style.setProperty('--color-primary', theme.colors.primary);
root.style.setProperty('--color-secondary', theme.colors.secondary);
root.style.setProperty('--color-accent', theme.colors.accent);
root.style.setProperty('--color-background', theme.colors.background);
root.style.setProperty('--color-text', theme.colors.text);
root.style.setProperty('--color-border', theme.colors.border);
root.style.setProperty('--color-success', theme.colors.success);
root.style.setProperty('--color-error', theme.colors.error);
root.style.setProperty('--color-warning', theme.colors.warning);
root.style.setProperty('--font-family', theme.typography.fontFamily);
root.style.setProperty('--font-heading', theme.typography.headingFont);
root.style.setProperty('--base-font-size', `${theme.typography.baseFontSize}px`);
root.style.setProperty('--spacing-unit', `${theme.spacing.unit}px`);
// Update favicon
const favicon = document.querySelector('link[rel="icon"]') as HTMLLinkElement;
if (favicon) {
favicon.href = theme.branding.faviconUrl;
}
// Update document title
document.title = theme.branding.companyName;
}
getCurrentTheme(): ThemeConfig {
return this.currentTheme;
}
subscribe(listener: (theme: ThemeConfig) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private notifyListeners(theme: ThemeConfig): void {
this.listeners.forEach(listener => listener(theme));
}
clearCache(): void {
this.themeCache.clear();
}
}
export const themeService = new ThemeService();
3. React Context for Theme
// context/ThemeContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { ThemeConfig, DEFAULT_THEME } from '../types/theme';
import { themeService } from '../services/themeService';
interface ThemeContextType {
theme: ThemeConfig;
loading: boolean;
error: string | null;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
interface ThemeProviderProps {
children: React.ReactNode;
clientId: string;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
children,
clientId,
}) => {
const [theme, setTheme] = useState<ThemeConfig>(DEFAULT_THEME);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadTheme = async () => {
try {
setLoading(true);
setError(null);
const loadedTheme = await themeService.loadTheme(clientId);
setTheme(loadedTheme);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load theme');
setTheme(DEFAULT_THEME);
} finally {
setLoading(false);
}
};
loadTheme();
// Subscribe to theme changes
const unsubscribe = themeService.subscribe(setTheme);
return unsubscribe;
}, [clientId]);
return (
<ThemeContext.Provider value={{ theme, loading, error }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
};
4. Styled Components with Theme
// styles/theme.ts
import styled from 'styled-components';
import { useTheme } from '../context/ThemeContext';
export const Container = styled.div`
background-color: var(--color-background);
color: var(--color-text);
font-family: var(--font-family);
font-size: var(--base-font-size);
padding: calc(var(--spacing-unit) * 2);
`;
export const Header = styled.header`
background-color: var(--color-primary);
color: white;
padding: calc(var(--spacing-unit) * 3);
border-bottom: 2px solid var(--color-border);
`;
export const Logo = styled.img`
height: 40px;
width: auto;
`;
export const Button = styled.button<{ variant?: 'primary' | 'secondary' }>`
background-color: ${props =>
props.variant === 'secondary'
? 'var(--color-secondary)'
: 'var(--color-primary)'};
color: white;
border: none;
padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 2);
border-radius: 4px;
font-family: var(--font-family);
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.9;
}
&:active {
opacity: 0.8;
}
`;
export const Card = styled.div`
background-color: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: calc(var(--spacing-unit) * 2);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
`;
export const Heading = styled.h1`
font-family: var(--font-heading);
color: var(--color-primary);
margin: 0 0 calc(var(--spacing-unit) * 2) 0;
`;
5. Client Detection Middleware
// middleware/clientDetection.ts
export interface ClientContext {
clientId: string;
subdomain?: string;
domain?: string;
}
export const detectClient = (): ClientContext => {
// Method 1: Subdomain-based (e.g., acme.app.com)
const hostname = window.location.hostname;
const parts = hostname.split('.');
if (parts.length > 2 && parts[0] !== 'www') {
return {
clientId: parts[0],
subdomain: parts[0],
domain: parts.slice(1).join('.'),
};
}
// Method 2: Query parameter (e.g., ?client=acme)
const params = new URLSearchParams(window.location.search);
const clientParam = params.get('client');
if (clientParam) {
return { clientId: clientParam };
}
// Method 3: Path-based (e.g., /clients/acme)
const pathMatch = window.location.pathname.match(/\/clients\/([^/]+)/);
if (pathMatch) {
return { clientId: pathMatch[1] };
}
// Method 4: From localStorage (for SPA)
const stored = localStorage.getItem('clientId');
if (stored) {
return { clientId: stored };
}
return { clientId: 'default' };
};
export const setClientContext = (clientId: string): void => {
localStorage.setItem('clientId', clientId);
};
6. App Component Integration
// App.tsx
import React, { useEffect, useState } from 'react';
import { ThemeProvider } from './context/ThemeContext';
import { detectClient, ClientContext } from './middleware/clientDetection';
import Dashboard from './pages/Dashboard';
import { Container } from './styles/theme';
const App: React.FC = () => {
const [clientContext, setClientContext] = useState<ClientContext | null>(null);
useEffect(() => {
const context = detectClient();
setClientContext(context);
}, []);
if (!clientContext) {
return <div>Loading...</div>;
}
return (
<ThemeProvider clientId={clientContext.clientId}>
<Container>
<Dashboard />
</Container>
</ThemeProvider>
);
};
export default App;
7. Example Component Using Theme
// components/BrandedHeader.tsx
import React from 'react';
import { useTheme } from '../context/ThemeContext';
import { Header, Logo, Heading } from '../styles/theme';
const BrandedHeader: React.FC = () => {
const { theme, loading } = useTheme();
if (loading) {
return <Header>Loading...</Header>;
}
return (
<Header>
<Logo src={theme.branding.logoUrl} alt={theme.branding.companyName} />
<Heading>{theme.branding.companyName}</Heading>
</Header>
);
};
export default BrandedHeader;
8. Backend Theme Configuration API
// Backend: Express.js example
import express from 'express';
import { ThemeConfig } from './types/theme';
const app = express();
// In-memory theme store (use database in production)
const themes: Map<string, ThemeConfig> = new Map([
[
'acme',
{
clientId: 'acme',
name: 'ACME Corp',
colors: {
primary: '#FF6B35',
secondary: '#004E89',
accent: '#F7931E',
background: '#FFFFFF',
text: '#1A1A1A',
border: '#E0E0E0',
success: '#06A77D',
error: '#D62828',
warning: '#F77F00',
},
typography: {
fontFamily: 'Roboto, sans-serif',
headingFont: 'Montserrat, sans-serif',
baseFontSize: 16,
},
branding: {
logoUrl: 'https://cdn.example.com/acme-logo.png',
faviconUrl: 'https://cdn.example.com/acme-favicon.ico',
companyName: 'ACME Corporation',
},
spacing: { unit: 8 },
},
],
[
'techstart',
{
clientId: 'techstart',
name: 'TechStart Inc',
colors: {
primary: '#6366F1',
secondary: '#EC4899',
accent: '#14B8A6',
background: '#0F172A',
text: '#F1F5F9',
border: '#334155',
success: '#10B981',
error: '#EF4444',
warning: '#F59E0B',
},
typography: {
fontFamily: 'Fira Sans, sans-serif',
headingFont: 'Space Grotesk, sans-serif',
baseFontSize: 16,
},
branding: {
logoUrl: 'https://cdn.example.com/techstart-logo.png',
faviconUrl: 'https://cdn.example.com/techstart-favicon.ico',
companyName: 'TechStart Inc',
},
spacing: { unit: 8 },
},
],
]);
app.get('/themes/:clientId', (req, res) => {
const { clientId } = req.params;
const theme = themes.get(clientId);
if (!theme) {
return res.status(404).json({ error: 'Theme not found' });
}
res.json(theme);
});
app.post('/themes/:clientId', (req, res) => {
const { clientId } = req.params;
const themeConfig: ThemeConfig = req.body;
themes.set(clientId, themeConfig);
res.json({ success: true, theme: themeConfig });
});
app.listen(3001, () => console.log('Theme API running on port 3001'));
9. CSS Variables Fallback
/* styles/global.css */
:root {
/* Colors */
--color-primary: #007AFF;
--color-secondary: #5AC8FA;
--color-accent: #FF2D55;
--color-background: #FFFFFF;
--color-text: #000000;
--color-border: #E5E5EA;
--color-success: #34C759;
--color-error: #FF3B30;
--color-warning: #FF9500;
/* Typography */
--font-family: 'Inter', sans-serif;
--font-heading: 'Poppins', sans-serif;
--base-font-size: 16px;
/* Spacing */
--spacing-unit: 8px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family);
font-size: var(--base-font-size);
color: var(--color-text);
background-color: var(--color-background);
}
button {
font-family: var(--font-family);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
}
Tips & Best Practices
1. Performance Optimization
- Cache themes aggressively with service workers
- Lazy load theme assets
- Use CSS variables for instant theme switching without re-renders
- Implement theme preloading on route transitions
2. Security Considerations
- Validate all theme configuration on the backend
- Sanitize user-provided URLs (logos, fonts)
- Use Content Security Policy (CSP) headers
- Never expose sensitive client data in theme configs
3. Scalability
- Store themes in a database with versioning
- Implement theme inheritance (child themes override parent)
- Use CDN for all branding assets
- Cache theme responses with appropriate TTLs
4. Developer Experience
- Provide theme builder UI for non-technical clients
- Export theme as JSON for easy sharing
- Create theme validation schema
- Document all available theme properties
5. Testing
// Example test
import { render } from '@testing-library/react';
import { ThemeProvider } from './context/ThemeContext';
import BrandedHeader from './components/BrandedHeader';
test('renders with custom theme', () => {
const { getByAltText } = render(
<ThemeProvider clientId="acme">
<BrandedHeader />
</ThemeProvider>
);
expect(getByAltText('ACME Corporation')).toBeInTheDocument();
});
6. Monitoring
- Track theme load times
- Monitor failed theme requests
- Alert on theme configuration errors
- Log theme switches for audit trails
7. Fallback Strategy
- Always have a default theme
- Implement graceful degradation
- Provide offline