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:
| Layer | Isolation Mechanism |
|---|---|
| API routing | All resource endpoints include the tenant slug in the URL path: /v3/resources/{tenant}/{type} |
| Authorization | OPA/Authz evaluates policies per-tenant; users must have a role within a tenant to access it |
| Data storage | ResourceAPI scopes all database queries by tenant; cross-tenant queries are not possible |
| Configuration | Object types, workflows, and tenant settings are stored per-tenant in Configurator |
| AI context | Chat 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:
- Creates
src/eai.config/tenants/permits.config.tswith default configuration - Registers the tenant in
src/eai.config/index.ts - Adds placeholder environment variables to
.env.local
After creating the tenant, you need to:
- Set the
tenantIdandworkflowIdfrom Configurator - Define object types for the tenant in
src/eai.config/object-types.ts - Seed the object types with
eai types seed --tenant permits - 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.