Tutorial: Create a White-Label Configuration
Configure a fully branded white-label tenant for a fictional client called "Acme Legal." You will learn how the tenant configuration system works, how to customize branding, layout, navigation, and feature flags, and how to test and deploy your white-label build.
30 minutes -- This tutorial covers the full white-label workflow from config creation through testing and deployment.
What You'll Build
- A tenant configuration file with custom branding
- Theme colors, logo, and typography overrides
- A customized layout with branded header, sidebar, and content areas
- Feature flags that enable and disable platform modules
- Custom navigation menus
- Tenant-specific CSS styles
Prerequisites
Before starting this tutorial, make sure you have:
- A working vertical project (see Build a Task Tracker)
- Authenticated with
eai login - Familiarity with the Tenant Configuration Reference
Step 1: Understand the Tenant Configuration System
Every vertical application is driven by a tenant configuration object that implements the EAIConfig interface. This single TypeScript file controls branding, layout, state management, and feature availability for a given client.
export interface EAIConfig {
// Required
tenantId: string; // Unique identifier, used in URLs and API calls
displayName: string; // Human-readable name shown in the UI
store: Record<string, StoreSlice>; // Global state structure
layout: LayoutConfig; // Component placement in UI slots
// Optional
theme?: ThemeConfig; // Colors, logo, fonts
features?: FeatureFlags; // Enable/disable platform capabilities
}
The platform resolves the active tenant at runtime via the ?tenant= query parameter, a subdomain, or the TENANT_DEFAULT_ID environment variable. All UI rendering, API routing, and feature gating flows from this single config object.
Step 2: Create the Tenant Config File
Create src/eai.config/tenants/acme-legal.config.ts:
import type { EAIConfig } from '../types';
export const acmeLegalConfig: EAIConfig = {
tenantId: 'acme-legal',
displayName: 'Acme Legal Services',
theme: {
primaryColor: '#1E3A5F',
secondaryColor: '#F0F4F8',
accentColor: '#2B7A78',
logo: '/tenants/acme-legal/logo.svg',
logoAlt: 'Acme Legal Services',
logoWidth: 160,
favicon: '/tenants/acme-legal/favicon.ico',
fontFamily: '"Source Sans 3", sans-serif',
headingFontFamily: '"Playfair Display", serif',
rootClassName: 'tenant-acme-legal',
},
store: {
user: {
initialState: {
name: null,
email: null,
role: null,
firm: 'Acme Legal Services',
},
persist: true,
},
ui: {
initialState: {
theme: 'light',
sidebarOpen: true,
},
persist: true,
},
},
layout: {
header: [],
leftPane: [],
middlePane: [],
rightPane: [],
},
features: {},
};
This is the skeleton. The following steps fill in each section.
Step 3: Configure Branding
The theme object you defined above controls all visual branding. Here is what each property does:
| Property | Purpose |
|---|---|
primaryColor | Buttons, links, active states |
secondaryColor | Backgrounds, card surfaces |
accentColor | Highlights, badges, secondary actions |
logo / logoAlt | Header logo image and alt text |
favicon | Browser tab icon |
fontFamily | Body text font stack |
headingFontFamily | Heading font stack |
rootClassName | CSS class applied to the root element for scoped styles |
Place the tenant assets in the public directory:
public/
tenants/
acme-legal/
logo.svg
logo-dark.svg
favicon.ico
Then create tenant-specific CSS overrides at src/app/tenants/acme-legal.css:
[data-tenant="acme-legal"] {
--primary: 213 52% 24%; /* #1E3A5F */
--primary-foreground: 0 0% 100%;
--secondary: 210 33% 96%; /* #F0F4F8 */
--accent: 178 47% 32%; /* #2B7A78 */
--accent-foreground: 0 0% 100%;
--font-sans: 'Source Sans 3', sans-serif;
--font-heading: 'Playfair Display', serif;
}
.tenant-acme-legal .header {
border-bottom: 3px solid #1E3A5F;
}
Import the CSS in src/app/layout.tsx:
import './tenants/acme-legal.css';
Step 4: Customize the Layout
Update the layout section of your config to define what components appear in each slot. The Vertical Template uses four layout slots: header, leftPane, middlePane, and rightPane.
layout: {
header: [
{
component: 'Header',
priority: 1,
props: {
logo: '/tenants/acme-legal/logo.svg',
logoAlt: 'Acme Legal Services',
showSearch: true,
showNotifications: true,
supportEmail: 'help@acmelegal.com',
},
storeBindings: [
{ prop: 'userName', storePath: 'user.name' },
{ prop: 'userRole', storePath: 'user.role' },
],
},
],
leftPane: [
{
component: 'Sidebar',
priority: 1,
props: {
brandColor: '#1E3A5F',
welcomeMessage: 'Acme Legal Portal',
},
storeBindings: [
{ prop: 'isOpen', storePath: 'ui.sidebarOpen' },
],
},
],
middlePane: [
{
component: 'BrandedWelcome',
priority: 1,
props: {
headline: 'Welcome to Acme Legal Services',
tagline: 'Manage your cases, documents, and deadlines in one place.',
ctaText: 'View Cases',
ctaHref: '/cases',
},
showWhen: (state) => !state.user?.name,
},
{
component: 'Dashboard',
priority: 2,
props: {
widgets: ['active-cases', 'upcoming-deadlines', 'recent-documents'],
},
showWhen: (state) => !!state.user?.name,
},
],
rightPane: [
{
component: 'HelpPanel',
priority: 1,
props: {
title: 'Acme Legal Support',
email: 'help@acmelegal.com',
phone: '1-800-555-0199',
articles: [
{ title: 'Getting Started', href: '/help/start' },
{ title: 'Filing a Case', href: '/help/cases' },
{ title: 'Document Upload', href: '/help/documents' },
],
},
},
],
},
The priority field controls render order within each slot. The showWhen function conditionally renders components based on global state -- in this case, showing a welcome screen for unauthenticated users and a dashboard for logged-in users.
Step 5: Set Up Feature Flags
Feature flags let you enable or disable entire platform modules per tenant. Add a features object to control what Acme Legal has access to:
features: {
// Core features
cases: true,
documents: true,
calendar: true,
// AI features
chat: true,
aiAssistant: true,
documentClassification: true,
// Disabled for this tenant
analytics: false,
experimentalFeatures: false,
},
In your components, gate features using the config:
import { useConfig } from '@enterpriseaigroup/client';
function CaseDashboard() {
const config = useConfig();
return (
<div>
<CaseList />
{config.features?.chat && <ChatWidget />}
{config.features?.aiAssistant && <AiAssistantPanel />}
</div>
);
}
Step 6: Configure Navigation
Navigation items are passed as props to the Header component. Update the header config's navItems prop:
header: [
{
component: 'Header',
priority: 1,
props: {
logo: '/tenants/acme-legal/logo.svg',
logoAlt: 'Acme Legal Services',
showSearch: true,
showNotifications: true,
navItems: [
{ label: 'Dashboard', href: '/dashboard', icon: 'home' },
{ label: 'Cases', href: '/cases', icon: 'briefcase' },
{ label: 'Documents', href: '/documents', icon: 'file-text' },
{ label: 'Calendar', href: '/calendar', icon: 'calendar' },
{ label: 'AI Assistant', href: '/chat', icon: 'message-circle' },
],
},
storeBindings: [
{ prop: 'userName', storePath: 'user.name' },
],
},
],
You can also add quick-action links to the sidebar:
leftPane: [
{
component: 'Sidebar',
priority: 1,
props: {
quickLinks: [
{ label: 'New Case', href: '/cases/new' },
{ label: 'Upload Document', href: '/documents/upload' },
{ label: 'Contact Support', href: '/support' },
],
},
storeBindings: [
{ prop: 'isOpen', storePath: 'ui.sidebarOpen' },
],
},
],
Step 7: Register and Test the Tenant
Register the config in src/eai.config/index.ts:
import { templateConfig } from './tenants/template.config';
import { acmeLegalConfig } from './tenants/acme-legal.config';
const configs: Record<string, EAIConfig> = {
template: templateConfig,
'acme-legal': acmeLegalConfig,
};
export function getTenantConfig(tenantId: string): EAIConfig {
return configs[tenantId] || configs.template;
}
Start the dev server and test:
eai dev
Open your browser to:
http://localhost:3000?tenant=acme-legal
Verify the following:
- The Acme Legal logo appears in the header
- Brand colors (#1E3A5F primary, #2B7A78 accent) are applied
- Navigation items match the config
- The welcome screen shows for unauthenticated users
- Feature-gated components render only when their flag is
true
You can also set Acme Legal as the default tenant during development:
# .env.local
TENANT_DEFAULT_ID=acme-legal
Step 8: Deploy with Tenant-Specific Configuration
For production deployments, set the tenant via environment variables on the hosting platform:
# Azure App Service / Environment Variables
TENANT_DEFAULT_ID=acme-legal
NEXT_PUBLIC_ENABLE_CHAT=true
NEXT_PUBLIC_ENABLE_AI=true
If you are deploying multiple tenants from the same codebase, the tenant is resolved at runtime from the ?tenant= query parameter or subdomain. No separate builds are needed.
For custom domain mapping (e.g., portal.acmelegal.com), configure your reverse proxy to inject the tenant header:
# Nginx example
location / {
proxy_pass http://localhost:3000;
proxy_set_header X-Tenant-Id acme-legal;
}
Deploy using the CLI:
eai deploy --env production
What You Learned
- EAIConfig Interface: The single TypeScript object that drives all tenant behavior
- Theme Configuration: Brand colors, logos, fonts, and tenant-scoped CSS
- Layout Slots: Placing components in header, leftPane, middlePane, and rightPane
- Feature Flags: Enabling and disabling modules per tenant
- Navigation: Configuring header nav items and sidebar quick links
- Tenant Resolution: Query parameter, subdomain, and environment variable strategies
- Deployment: Tenant-specific environment configuration
Next Steps
- Industry Examples -- See how tenants are configured for different industries
- Theming Reference -- Full CSS variable reference
- Feature Flags -- Advanced flag patterns
- White-Label Example -- Complete reference config