Skip to main content

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.

Estimated Time

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:

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:

PropertyPurpose
primaryColorButtons, links, active states
secondaryColorBackgrounds, card surfaces
accentColorHighlights, badges, secondary actions
logo / logoAltHeader logo image and alt text
faviconBrowser tab icon
fontFamilyBody text font stack
headingFontFamilyHeading font stack
rootClassNameCSS 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