Features
Multi-Tenant Management
Comprehensive guide to Company Manager's multi-tenant architecture, tenant and site management, and user access control.
Multi-Tenant Management
Company Manager is built with a sophisticated multi-tenant architecture that allows multiple organizations to use the platform while maintaining complete data isolation and customization.
🏗️ Architecture Overview
Tenant Hierarchy
Tenant (Organization)
└── Sites (Locations/Divisions)
├── Users (Staff Members)
├── Clients (Customers)
├── Orders (Transactions)
├── Products (Inventory)
└── Vendors (Suppliers)Key Concepts
Tenant
- Definition: Top-level organization or company
- Purpose: Complete data isolation between different organizations
- Examples: "Acme Corp", "TechStart Inc", "Local Restaurant"
Site
- Definition: Sub-organization within a tenant
- Purpose: Departmental or location-based organization
- Examples: "Main Office", "Warehouse", "Store #1"
User Roles
- Tenant Level: SUPER_ADMIN, ADMIN
- Site Level: MANAGER, USER, VIEWER
- Cross-Tenant: Users can belong to multiple tenants with different roles
🔧 Implementation Details
Database Schema
Tenant Model
model Tenant {
id String @id @default(dbgenerated("uuid_generate_v7()"))
name String // Organization name
slug String @unique // URL-friendly identifier
description String?
status TenantStatus @default(ACTIVE)
settings Json? // Tenant-specific configuration
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relationships
sites Site[]
tenantUsers TenantUser[]
clients Client[]
orders Order[]
products Product[]
vendors Vendor[]
@@map("tenants")
}Site Model
model Site {
id String @id @default(dbgenerated("uuid_generate_v7()"))
name String // Site name
slug String // URL-friendly identifier
description String?
status SiteStatus @default(PENDING)
settings Json? // Site-specific configuration
tenantId String // Foreign key to tenant
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
siteUsers SiteUser[]
clients Client[]
orders Order[]
@@unique([tenantId, slug])
@@map("sites")
}User Associations
model TenantUser {
id String @id @default(dbgenerated("uuid_generate_v7()"))
tenantId String
userId String
role UserRole // SUPER_ADMIN, ADMIN, etc.
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([tenantId, userId])
@@map("tenant_users")
}
model SiteUser {
id String @id @default(dbgenerated("uuid_generate_v7()"))
siteId String
userId String
role UserRole // MANAGER, USER, VIEWER
site Site @relation(fields: [siteId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([siteId, userId])
@@map("site_users")
}Data Isolation Strategy
Query Filtering
All business data queries include tenant and site filters:
// Example: Get clients for current tenant/site
const clients = await db.client.findMany({
where: {
tenantId: currentTenant.id,
siteId: currentSite?.id, // Optional site filter
},
});
// Example: Create order with tenant context
const order = await db.order.create({
data: {
invoiceNumber: "INV-2024-001",
tenantId: currentTenant.id,
siteId: currentSite?.id,
clientId: "client-123",
// ... other fields
},
});Middleware Protection
// TRPC middleware for tenant isolation
const tenantMiddleware = t.middleware(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const tenantAccess = await db.tenantUser.findFirst({
where: {
userId: ctx.user.id,
tenantId: ctx.tenantId,
},
});
if (!tenantAccess) {
throw new TRPCError({ code: "FORBIDDEN" });
}
return next({
ctx: {
...ctx,
tenantAccess,
},
});
});🎯 Tenant Management Features
Tenant Creation
// Create new tenant with default site
export async function createTenant(data: CreateTenantInput) {
const tenant = await db.tenant.create({
data: {
name: data.name,
slug: generateSlug(data.name),
description: data.description,
status: "ACTIVE",
settings: {
theme: "default",
features: {
invoicing: true,
inventory: true,
crm: true,
},
},
},
});
// Create default site
const site = await db.site.create({
data: {
name: "Main Office",
slug: "main",
tenantId: tenant.id,
status: "ACTIVE",
},
});
return { tenant, site };
}Tenant Configuration
// Tenant settings interface
interface TenantSettings {
branding: {
logo?: string;
primaryColor: string;
secondaryColor: string;
};
features: {
invoicing: boolean;
inventory: boolean;
crm: boolean;
analytics: boolean;
};
integrations: {
stripe?: StripeConfig;
mailgun?: MailgunConfig;
webhook?: WebhookConfig;
};
preferences: {
timezone: string;
currency: string;
dateFormat: string;
language: string;
};
}Site Management
// Create site within tenant
export async function createSite(tenantId: string, data: CreateSiteInput) {
return db.site.create({
data: {
name: data.name,
slug: data.slug,
description: data.description,
tenantId,
status: "ACTIVE",
settings: {
address: data.address,
contactInfo: data.contactInfo,
operatingHours: data.operatingHours,
},
},
});
}👥 User Access Control
Role-Based Access Control (RBAC)
Tenant Roles
- SUPER_ADMIN: Full access across all tenants (platform admin)
- ADMIN: Full access within tenant, can manage sites and users
- MANAGER: Site management, user management within sites
- USER: Standard business operations
- VIEWER: Read-only access
Permission Matrix
const permissions = {
SUPER_ADMIN: ["*"], // All permissions
ADMIN: [
"tenant:read",
"tenant:update",
"site:*",
"user:*",
"client:*",
"order:*",
"product:*",
],
MANAGER: [
"site:read",
"site:update",
"user:read",
"user:create",
"client:*",
"order:*",
],
USER: [
"client:read",
"client:create",
"client:update",
"order:read",
"order:create",
],
VIEWER: ["client:read", "order:read", "product:read"],
};User Invitation System
// Invite user to tenant
export async function inviteUserToTenant(
tenantId: string,
email: string,
role: UserRole,
siteIds?: string[]
) {
// Create invitation
const invitation = await db.invitation.create({
data: {
email,
role,
tenantId,
siteIds,
token: generateInvitationToken(),
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
},
});
// Send invitation email
await sendInvitationEmail(email, invitation.token);
return invitation;
}
// Accept invitation
export async function acceptInvitation(token: string, userId: string) {
const invitation = await db.invitation.findFirst({
where: {
token,
expiresAt: { gt: new Date() },
acceptedAt: null,
},
});
if (!invitation) {
throw new Error("Invalid or expired invitation");
}
// Add user to tenant
await db.tenantUser.create({
data: {
tenantId: invitation.tenantId,
userId,
role: invitation.role,
},
});
// Add user to sites if specified
if (invitation.siteIds?.length) {
await db.siteUser.createMany({
data: invitation.siteIds.map((siteId) => ({
siteId,
userId,
role: invitation.role,
})),
});
}
// Mark invitation as accepted
await db.invitation.update({
where: { id: invitation.id },
data: { acceptedAt: new Date() },
});
}🔄 Tenant Switching
Frontend Implementation
// Tenant context provider
export function TenantProvider({ children }: { children: React.ReactNode }) {
const [currentTenant, setCurrentTenant] = useState<Tenant | null>(null)
const [currentSite, setCurrentSite] = useState<Site | null>(null)
const [userTenants, setUserTenants] = useState<TenantUser[]>([])
const switchTenant = useCallback(async (tenantId: string, siteId?: string) => {
// Validate access
const tenantAccess = userTenants.find(t => t.tenantId === tenantId)
if (!tenantAccess) {
throw new Error('Access denied to tenant')
}
// Update context
setCurrentTenant(tenantAccess.tenant)
if (siteId) {
const site = await api.site.getById.query({ id: siteId })
setCurrentSite(site)
}
// Update URL
router.push(`/app/${tenantId}/${siteId || ''}`)
}, [userTenants, router])
return (
<TenantContext.Provider value={{
currentTenant,
currentSite,
userTenants,
switchTenant,
}}>
{children}
</TenantContext.Provider>
)
}Tenant Switcher Component
export function TenantSwitcher() {
const { currentTenant, userTenants, switchTenant } = useTenant()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full justify-between">
{currentTenant?.name || 'Select Tenant'}
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
{userTenants.map((tenantUser) => (
<DropdownMenuItem
key={tenantUser.tenantId}
onClick={() => switchTenant(tenantUser.tenantId)}
>
<div className="flex flex-col">
<span className="font-medium">{tenantUser.tenant.name}</span>
<span className="text-sm text-muted-foreground">
{tenantUser.role}
</span>
</div>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}📊 Multi-Tenant Analytics
Tenant-Specific Metrics
// Get tenant analytics
export async function getTenantAnalytics(tenantId: string) {
const [totalClients, totalOrders, totalRevenue, activeUsers] =
await Promise.all([
db.client.count({ where: { tenantId } }),
db.order.count({ where: { tenantId } }),
db.order.aggregate({
where: { tenantId, invoiceStatus: "PAID" },
_sum: { totalAmount: true },
}),
db.tenantUser.count({ where: { tenantId } }),
]);
return {
clients: totalClients,
orders: totalOrders,
revenue: totalRevenue._sum.totalAmount || 0,
users: activeUsers,
};
}Cross-Tenant Reporting (Admin Only)
// Platform-wide analytics for super admins
export async function getPlatformAnalytics() {
const tenantStats = await db.tenant.findMany({
select: {
id: true,
name: true,
status: true,
_count: {
select: {
tenantUsers: true,
clients: true,
orders: true,
},
},
},
});
return tenantStats.map((tenant) => ({
...tenant,
users: tenant._count.tenantUsers,
clients: tenant._count.clients,
orders: tenant._count.orders,
}));
}🔒 Security Considerations
Data Isolation
- All queries include tenant/site filtering
- Middleware validates user access to tenant
- Database constraints prevent cross-tenant data access
- API endpoints validate tenant context
Access Control
- Role-based permissions at tenant and site level
- Invitation-based user onboarding
- Regular access reviews and auditing
- Session management with tenant context
Compliance
- GDPR compliance with tenant-specific data handling
- Data retention policies per tenant
- Export/import functionality for data portability
- Audit logging for all tenant operations
🚀 Best Practices
Development Guidelines
- Always include tenant filtering in database queries
- Validate tenant access in all API endpoints
- Use tenant-aware components in the frontend
- Test cross-tenant isolation thoroughly
- Implement proper error handling for tenant switching
Performance Optimization
- Index tenant and site fields for faster queries
- Use connection pooling for database efficiency
- Cache tenant configurations for better performance
- Implement pagination for large tenant datasets
- Monitor query performance across tenants
Monitoring
- Track tenant usage and performance metrics
- Monitor API response times per tenant
- Set up alerts for tenant-specific issues
- Log tenant switching and access patterns
- Regular security audits of tenant isolation
📚 API Reference
Tenant Management Endpoints
GET /api/tenants- List user's tenantsPOST /api/tenants- Create new tenantGET /api/tenants/:id- Get tenant detailsPUT /api/tenants/:id- Update tenantDELETE /api/tenants/:id- Delete tenant
Site Management Endpoints
GET /api/tenants/:tenantId/sites- List tenant sitesPOST /api/tenants/:tenantId/sites- Create siteGET /api/sites/:id- Get site detailsPUT /api/sites/:id- Update siteDELETE /api/sites/:id- Delete site
User Management Endpoints
POST /api/tenants/:tenantId/invite- Invite userGET /api/tenants/:tenantId/users- List tenant usersPUT /api/tenants/:tenantId/users/:userId- Update user roleDELETE /api/tenants/:tenantId/users/:userId- Remove user
For more information on implementing multi-tenant features, see: