@company-manager/docs
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

  1. Always include tenant filtering in database queries
  2. Validate tenant access in all API endpoints
  3. Use tenant-aware components in the frontend
  4. Test cross-tenant isolation thoroughly
  5. Implement proper error handling for tenant switching

Performance Optimization

  1. Index tenant and site fields for faster queries
  2. Use connection pooling for database efficiency
  3. Cache tenant configurations for better performance
  4. Implement pagination for large tenant datasets
  5. Monitor query performance across tenants

Monitoring

  1. Track tenant usage and performance metrics
  2. Monitor API response times per tenant
  3. Set up alerts for tenant-specific issues
  4. Log tenant switching and access patterns
  5. Regular security audits of tenant isolation

📚 API Reference

Tenant Management Endpoints

  • GET /api/tenants - List user's tenants
  • POST /api/tenants - Create new tenant
  • GET /api/tenants/:id - Get tenant details
  • PUT /api/tenants/:id - Update tenant
  • DELETE /api/tenants/:id - Delete tenant

Site Management Endpoints

  • GET /api/tenants/:tenantId/sites - List tenant sites
  • POST /api/tenants/:tenantId/sites - Create site
  • GET /api/sites/:id - Get site details
  • PUT /api/sites/:id - Update site
  • DELETE /api/sites/:id - Delete site

User Management Endpoints

  • POST /api/tenants/:tenantId/invite - Invite user
  • GET /api/tenants/:tenantId/users - List tenant users
  • PUT /api/tenants/:tenantId/users/:userId - Update user role
  • DELETE /api/tenants/:tenantId/users/:userId - Remove user

For more information on implementing multi-tenant features, see: