Skip to main content

Your First Vertical

This hands-on guide walks you through building your first feature - creating a new tenant configuration. By the end, you'll understand the core concepts of config-driven UI development.

What You'll Learn

  • How tenant configurations work
  • The config-driven UI pattern
  • Store slices and state management
  • Component rendering via slots

Prerequisites

  • Completed the Quickstart
  • Development server running (npm run dev)

Step 1: Understand the Template Config

Open src/eai.config/tenants/template.config.ts - this is the example tenant configuration.

// Key parts of a tenant config:
export const templateConfig: EAIConfig = {
tenantId: 'template', // Unique identifier
displayName: 'Template', // UI display name

// State management - define your app's global state
store: {
property: {
initialState: { selectedAddress: null },
persist: true, // Survives page refresh
},
ui: {
initialState: { sidebarOpen: true },
persist: false, // Resets on refresh
},
},

// Layout - which components render in which slots
layout: {
header: [...], // Top navigation
leftPane: [...], // Left sidebar
middlePane: [...], // Main content
rightPane: [...], // Right sidebar
},
};

Step 2: Create Your First Tenant

Create a new file src/eai.config/tenants/my-vertical.config.ts:

import type { EAIConfig } from '../types';

export const myVerticalConfig: EAIConfig = {
tenantId: 'my-vertical',
displayName: 'My Vertical',

// Define your application state
store: {
user: {
initialState: {
name: null,
email: null,
preferences: {},
},
persist: true,
},
ui: {
initialState: {
theme: 'light',
sidebarCollapsed: false,
},
persist: true,
},
},

// Define your UI layout
layout: {
header: [
{
component: 'Header',
priority: 1,
props: {
title: 'My Vertical',
showUserMenu: true,
},
storeBindings: [
{ prop: 'userName', storePath: 'user.name' },
],
},
],
leftPane: [],
middlePane: [
{
component: 'WelcomeCard',
priority: 1,
props: {
message: 'Welcome to My Vertical!',
},
showWhen: (state) => !state.user?.name,
},
],
rightPane: [],
},
};

Step 3: Register Your Tenant

Edit src/eai.config/index.ts to add your new config:

import { templateConfig } from './tenants/template.config';
import { myVerticalConfig } from './tenants/my-vertical.config';

const configs: Record<string, EAIConfig> = {
template: templateConfig,
'my-vertical': myVerticalConfig, // Add this line
};

export function getTenantConfig(tenantId: string): EAIConfig {
return configs[tenantId] || configs.template;
}

Step 4: Test Your Configuration

Add ?tenant=my-vertical to your URL:

http://localhost:3000?tenant=my-vertical

You should see your custom header title "My Vertical" instead of the default.

Step 5: Add Store Interaction

Let's add a component that reads and writes to the store.

Create src/components/UserForm.tsx:

'use client';

import { useStoreValue, useSetStore } from '@enterpriseaigroup/client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useState } from 'react';

export function UserForm() {
const currentName = useStoreValue('user.name');
const setStore = useSetStore();
const [inputName, setInputName] = useState('');

const handleSubmit = () => {
setStore('user.name', inputName, 'UserForm');
setInputName('');
};

return (
<div className="p-4 space-y-4">
<p>Current Name: {currentName || 'Not set'}</p>
<Input
value={inputName}
onChange={(e) => setInputName(e.target.value)}
placeholder="Enter your name"
/>
<Button onClick={handleSubmit}>Save Name</Button>
</div>
);
}

Step 6: Register the Component

Add to the ComponentRegistry in packages/client/src/config/ComponentRegistry.ts:

import { UserForm } from '@/components/UserForm';

registry.set('UserForm', UserForm);

Step 7: Add to Your Layout

Update your tenant config to include the form:

middlePane: [
{
component: 'UserForm',
priority: 1,
},
{
component: 'WelcomeCard',
priority: 2,
props: {
message: 'Welcome to My Vertical!',
},
showWhen: (state) => !state.user?.name,
},
],

Understanding What Happened

  1. Config-Driven UI: The layout defined in your config determines what renders
  2. Store Slices: user and ui slices hold your app state
  3. Store Bindings: storeBindings automatically pass store values as props
  4. Conditional Rendering: showWhen controls component visibility based on state
  5. Hooks: useStoreValue reads state, useSetStore writes to it

Key Concepts Reference

ConceptPurposeExample
tenantIdIdentifies the tenant'my-vertical'
storeDefines global state{ user: { initialState: {...} } }
layoutComponent placement{ header: [...], middlePane: [...] }
storeBindingsConnect store to props[{ prop: 'name', storePath: 'user.name' }]
showWhenConditional rendering(state) => state.user?.name
priorityRender order (lower = first)1, 2, 3...

Next Steps

You've created your first tenant configuration. Continue learning:

Exercises

Try these to reinforce your learning:

  1. Add a theme toggle: Use ui.theme to switch between light/dark
  2. Add preferences: Store user preferences in user.preferences
  3. Conditional header: Show different header based on auth state
  4. Multiple tenants: Create another tenant with different branding