Skip to main content

Tenant Management

The EnterpriseAI platform is multi-tenant by design. Every resource, configuration, and AI interaction is scoped to a specific tenant, ensuring complete data isolation. A single vertical application can serve multiple tenants, each with their own branding, navigation, features, and data.

What Tenants Are

A tenant represents an isolated environment within the platform. Each tenant has:

  • Its own data -- Resources are scoped by tenant; no cross-tenant data access is possible
  • Its own configuration -- Branding, navigation, feature flags, and workflow settings
  • Its own object types -- Each tenant can have a different data model
  • Its own authorization policies -- RBAC rules are evaluated per-tenant
  • Its own AI workflows -- Chat prompts, RAG contexts, and document classification rules

Tenants are identified by a slug (e.g., my-tenant, permits-dev, grants-prod) that appears in all API paths and configuration references.

How Tenant Isolation Works

Tenant isolation is enforced at multiple layers of the platform:

LayerIsolation Mechanism
API routingAll resource endpoints include the tenant slug in the URL path: /v3/resources/{tenant}/{type}
AuthorizationOPA/Authz evaluates policies per-tenant; users must have a role within a tenant to access it
Data storageResourceAPI scopes all database queries by tenant; cross-tenant queries are not possible
ConfigurationObject types, workflows, and tenant settings are stored per-tenant in Configurator
AI contextChat workflows and RAG indexes are scoped to the tenant

Tenant Configuration

Each tenant is configured in a TypeScript file at src/eai.config/tenants/{slug}.config.ts. The configuration controls the tenant's appearance, navigation, and feature set.

// src/eai.config/tenants/permits.config.ts
import { TenantConfig } from '../types';

export const permitsConfig: TenantConfig = {
slug: 'permits',
displayName: 'Building Permits Portal',
description: 'Online building permit application and tracking system',

// Branding
branding: {
primaryColor: '#1a56db',
logo: '/assets/permits-logo.svg',
favicon: '/assets/permits-favicon.ico',
},

// Navigation
navigation: [
{
label: 'Dashboard',
path: '/dashboard',
icon: 'LayoutDashboard',
},
{
label: 'Applications',
path: '/applications',
icon: 'FileText',
},
{
label: 'Inspections',
path: '/inspections',
icon: 'ClipboardCheck',
},
],

// Feature flags
features: {
chat: true,
documentUpload: true,
notifications: false,
},

// Platform IDs (from Configurator)
tenantId: '<configurator-tenant-id>',
workflowId: '<configurator-workflow-id>',
};

Registering the Tenant

After creating the configuration file, register the tenant in the config registry at src/eai.config/index.ts:

// src/eai.config/index.ts
import { permitsConfig } from './tenants/permits.config';
import { grantsConfig } from './tenants/grants.config';

export const tenantConfigs = {
permits: permitsConfig,
grants: grantsConfig,
};

The tenant slug is also added to the TENANT_KEYS environment variable:

TENANT_KEYS=permits,grants
TENANT_PERMITS_ID=<configurator-tenant-id>
WORKFLOW_PERMITS_ID=<configurator-workflow-id>
TENANT_GRANTS_ID=<configurator-tenant-id>
WORKFLOW_GRANTS_ID=<configurator-workflow-id>

Creating Tenants

Use the EAI CLI to create a new tenant:

# Create a new tenant configuration file
eai tenant create permits --display-name "Building Permits Portal"

This command:

  1. Creates src/eai.config/tenants/permits.config.ts with default configuration
  2. Registers the tenant in src/eai.config/index.ts
  3. Adds placeholder environment variables to .env.local

After creating the tenant, you need to:

  1. Set the tenantId and workflowId from Configurator
  2. Define object types for the tenant in src/eai.config/object-types.ts
  3. Seed the object types with eai types seed --tenant permits
  4. Customize the branding, navigation, and features

Tenant Switching

A vertical application can support multiple tenants. The active tenant is determined by:

Query Parameter

The simplest approach -- the tenant slug is passed as a query parameter:

https://myapp.example.com/dashboard?tenant=permits
https://myapp.example.com/dashboard?tenant=grants

Subdomain Routing

For production deployments, tenants can be mapped to subdomains:

https://permits.myapp.example.com/dashboard
https://grants.myapp.example.com/dashboard

Programmatic Switching

In code, the active tenant can be read and switched using the config registry:

import { tenantConfigs } from '@/eai.config';

// Get the current tenant from the request context
function getTenantConfig(tenantSlug: string) {
const config = tenantConfigs[tenantSlug];
if (!config) {
throw new Error(`Unknown tenant: ${tenantSlug}`);
}
return config;
}

// Use in a Server Component
export default async function DashboardPage({
searchParams,
}: {
searchParams: { tenant?: string };
}) {
const tenantSlug = searchParams.tenant || 'default';
const tenant = getTenantConfig(tenantSlug);

return (
<div>
<h1>{tenant.displayName}</h1>
{/* Render tenant-specific content */}
</div>
);
}

Per-Tenant Data Isolation

When using the Platform SDK or React hooks, the tenant slug is included in every API call. This ensures that data operations are always scoped to the correct tenant:

// SDK -- tenant is set at client initialization
const client = new EAIPlatformClient({ tenantId: 'permits' });

// All operations are automatically scoped to the "permits" tenant
const apps = await client.resources.list('Application');
// Calls: GET /v3/resources/permits/Application
// React hook -- tenant is read from context or config
const { list } = useResources<ApplicationData>('Application');

// The hook reads the tenant from the application context
const result = await list({ page: 1, limit: 20 });

Even if a malicious request attempted to access another tenant's data by modifying the URL path, the Authz service would reject it because the user's JWT does not grant access to that tenant.

Per-Tenant Object Types

Each tenant can have its own set of object types. In src/eai.config/object-types.ts, types are organized by tenant slug:

export const objectTypes = {
permits: [
{ name: 'Application', /* ... */ },
{ name: 'Inspection', /* ... */ },
{ name: 'Document', /* ... */ },
],
grants: [
{ name: 'GrantApplication', /* ... */ },
{ name: 'FundingSource', /* ... */ },
{ name: 'Report', /* ... */ },
],
};

This allows a single vertical application to serve fundamentally different use cases across tenants, each with its own data model, while sharing the same UI framework and platform infrastructure.