Skip to content

AI Prompt: Forma3D.Connect — Full Multi-Tenancy + Super Admin

Purpose: Instruct an AI (Claude Opus 4.5) to implement complete multi-tenancy with tenant switching and super admin capabilities
Estimated Effort: 24–40 hours (implementation + tests)
Prerequisites: Phase RBAC-multitenancy (single-tenant mode) completed
Output: True multi-tenant system where super admins can manage and switch between tenants, tenants are fully isolated, and all API calls are tenant-scoped
Status: 🚧 TODO


🎯 Mission

Implement full multi-tenancy in Forma3D.Connect, extending the existing tenant-ready infrastructure to support:

  1. Multiple active tenants with complete data isolation
  2. Super Admin role that can view and manage all tenants
  3. Tenant switching for super admins after login
  4. Per-tenant integrations (Shopify shops, SimplyPrint accounts, Sendcloud credentials)
  5. Tenant-scoped API enforcement with no cross-tenant data leakage

Critical constraints:

  • Build on top of existing RBAC and tenant-ready infrastructure
  • Maintain backward compatibility with current single-tenant deployments
  • Super Admin is a system-level role, not a tenant-level role
  • All existing API endpoints must remain functional
  • Shopify OAuth shops are already tenant-scoped; leverage this pattern

Deliverables:

  • Super Admin can log in and see a tenant selector UI
  • Super Admin can switch active tenant context at any time
  • Regular users only see their own tenant's data
  • All DTOs include tenantId for proper context propagation
  • All repository update/delete operations enforce tenant isolation
  • Webhook routing supports multiple tenants (Shopify shop → tenant mapping)

📌 Context (Current State)

What Exists

From the RBAC-multitenancy phase:

  • Tenant table with a default tenant seeded
  • tenantId foreign keys on all tenant-owned entities (Order, PrintJob, Shipment, etc.)
  • TenantContextService that resolves tenant from authenticated user
  • User table with roles and permissions
  • Local user authentication with session cookies
  • Permission-based access control via @RequirePermissions decorator

What's Missing (Identified Issues)

  1. DTOs missing tenantId
  2. OrderDto, PrintJobDto, ShipmentDto, LineItemDto in libs/domain-contracts don't include tenantId
  3. This caused the fulfillment service to need the full Prisma Order instead of the lightweight DTO

  4. Update operations without tenant filtering

  5. OrdersRepository.updateStatus(), update(), updateFulfillment() use where: { id } without tenantId
  6. PrintJobsRepository.update() same issue
  7. ShipmentsRepository.update(), updateByOrderId() same issue
  8. Security risk: These can modify any tenant's data if the record ID is known

  9. No Super Admin role

  10. Current roles (admin, operator, viewer) are tenant-scoped
  11. No mechanism for cross-tenant access

  12. No tenant switching UI

  13. Users are locked to their assigned tenant
  14. No way to view or switch between tenants

  15. Services don't consistently use TenantContext

  16. Some services access repositories without passing tenantId
  17. Repositories default to DEFAULT_TENANT_ID when tenantId omitted

🛠️ Tech Stack Reference

Same as existing stack:

  • Backend: NestJS (TypeScript), Prisma, PostgreSQL
  • Frontend: React 19 + React Router + TanStack Query
  • Monorepo: Nx + pnpm
  • Testing: Jest (API), Vitest (web), Playwright acceptance tests

🏗️ Architecture Requirements

1) Super Admin Concept

The Super Admin is a system-level privileged user who:

  • Can view a list of all tenants
  • Can switch their "active tenant context" after login
  • When operating within a tenant, has full admin permissions for that tenant
  • Can create new tenants
  • Can manage tenant-level integrations (Shopify shops, API keys)
  • Can view cross-tenant reports/dashboards (optional, future)

Implementation approach:

  • Add isSuperAdmin boolean flag to the User model
  • Super Admin is orthogonal to tenant roles (a super admin can ALSO have tenant-specific roles)
  • Super Admin has an "active tenant" in their session that can be changed
  • API requests use the active tenant from session, not the user's tenantId

2) Tenant Isolation (Non-Negotiable)

Every repository method that reads/writes tenant-owned data must:

  • Require tenantId as a parameter (not optional)
  • Include tenantId in the where clause for all queries
  • Validate the tenant ID before operations

For update/delete operations, use compound where clauses:

// ❌ WRONG - can modify any tenant's data
await prisma.order.update({
  where: { id },
  data: { status },
});

// ✅ CORRECT - enforces tenant isolation
await prisma.order.update({
  where: { id, tenantId }, // Compound where
  data: { status },
});

3) DTO Contracts

All domain DTOs must include tenantId:

export interface OrderDto {
  id: string;
  tenantId: string; // ← REQUIRED
  shopifyOrderId: string;
  // ... rest of fields
}

4) Webhook Multi-Tenant Routing

For webhooks from external services:

  • Shopify: Map x-shopify-shop-domain header → ShopifyShop.shopDomaintenantId
  • SimplyPrint: Map webhook token or path → tenant (TBD based on integration)
  • Sendcloud: Map secret token → tenant (TBD based on integration)

📁 Files to Create/Modify

Backend (NestJS)

apps/api/src/
├── auth/
│   ├── decorators/
│   │   ├── current-user.decorator.ts      # UPDATE: include activeTenantId
│   │   └── require-super-admin.decorator.ts  # NEW
│   ├── guards/
│   │   └── super-admin.guard.ts           # NEW
│   └── services/
│       └── auth.service.ts                # UPDATE: handle super admin session
│
├── tenancy/
│   ├── dto/
│   │   ├── tenant.dto.ts                  # NEW: tenant response DTO
│   │   ├── create-tenant.dto.ts           # NEW: create tenant input
│   │   └── switch-tenant.dto.ts           # NEW: switch tenant input
│   ├── tenants.controller.ts              # NEW: tenant management endpoints
│   ├── tenants.service.ts                 # NEW: tenant business logic
│   ├── tenants.repository.ts              # NEW: tenant data access
│   ├── tenant-context.service.ts          # UPDATE: support super admin context
│   └── tenancy.module.ts                  # UPDATE: export new services
│
├── orders/
│   ├── orders.repository.ts               # UPDATE: add tenantId to all methods
│   ├── orders.service.ts                  # UPDATE: pass tenantId from context
│   └── orders.controller.ts               # UPDATE: inject TenantContext
│
├── print-jobs/
│   ├── print-jobs.repository.ts           # UPDATE: add tenantId to all methods
│   └── print-jobs.service.ts              # UPDATE: pass tenantId from context
│
├── shipments/
│   ├── shipments.repository.ts            # UPDATE: add tenantId to all methods
│   └── shipments.service.ts               # UPDATE: pass tenantId from context
│
├── fulfillment/
│   └── fulfillment.service.ts             # UPDATE: use OrderDto with tenantId
│
└── shopify/
    └── shopify-webhook.service.ts         # UPDATE: resolve tenant from shop domain

Domain Contracts (libs/domain-contracts)

libs/domain-contracts/src/lib/
├── types.ts                               # UPDATE: add tenantId to all DTOs
└── contracts/
    └── tenant.contracts.ts                # NEW: tenant-related interfaces

Frontend (React Dashboard)

apps/web/src/
├── auth/
│   ├── auth-context.tsx                   # UPDATE: include activeTenantId, isSuperAdmin
│   └── hooks/
│       └── use-auth.ts                    # UPDATE: expose tenant switching
│
├── components/
│   └── tenant-switcher/
│       ├── tenant-switcher.tsx            # NEW: dropdown to switch tenants
│       └── tenant-switcher.api.ts         # NEW: API calls for tenant switching
│
├── pages/
│   ├── admin/
│   │   └── tenants/
│   │       ├── tenants-list.tsx           # NEW: list all tenants (super admin)
│   │       ├── tenant-create.tsx          # NEW: create tenant form
│   │       └── tenant-detail.tsx          # NEW: view/edit tenant
│   └── layout/
│       └── header.tsx                     # UPDATE: show tenant switcher for super admin
│
└── router.tsx                             # UPDATE: add tenant management routes

Database (Prisma)

prisma/
├── schema.prisma                          # UPDATE: add isSuperAdmin to User
├── seed.ts                                # UPDATE: create super admin user
└── migrations/                            # NEW: migration for isSuperAdmin

🔐 Super Admin Implementation

1) Database Schema Changes

model User {
  id            String   @id @default(uuid())
  tenantId      String   // Home tenant (for regular users)
  email         String   @unique
  passwordHash  String
  name          String?

  // Super Admin flag - allows cross-tenant access
  isSuperAdmin  Boolean  @default(false)

  // Session state for super admin (which tenant they're currently viewing)
  // Note: This is stored in session, not in User table

  roles         UserRole[]
  // ... rest of relations
}

2) Session Structure for Super Admin

Extend the session to include active tenant:

interface UserSession {
  userId: string;
  email: string;
  tenantId: string; // User's home tenant
  isSuperAdmin: boolean;
  activeTenantId: string; // Currently active tenant (for super admin, can differ from tenantId)
  roles: string[];
  permissions: string[];
}

3) Tenant Context Resolution (Updated)

Update TenantContextService:

@Injectable({ scope: Scope.REQUEST })
export class TenantContextService {
  private tenantId: string | null = null;

  constructor(@Inject(REQUEST) private readonly request: Request) {
    // Resolve tenant from authenticated session
    const user = this.request.user as UserSession | undefined;

    if (user) {
      // For super admin, use activeTenantId (which they can switch)
      // For regular users, use their tenantId
      this.tenantId = user.isSuperAdmin ? user.activeTenantId : user.tenantId;
    }
  }

  getCurrentTenantId(): string {
    if (!this.tenantId) {
      throw new UnauthorizedException('No tenant context available');
    }
    return this.tenantId;
  }

  // For super admin to verify they can access a specific tenant
  canAccessTenant(tenantId: string): boolean {
    const user = this.request.user as UserSession | undefined;
    if (!user) return false;
    if (user.isSuperAdmin) return true;
    return user.tenantId === tenantId;
  }
}

4) Super Admin Guard

Create apps/api/src/auth/guards/super-admin.guard.ts:

import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';

@Injectable()
export class SuperAdminGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const user = request.user;

    if (!user?.isSuperAdmin) {
      throw new ForbiddenException('Super Admin access required');
    }

    return true;
  }
}

5) Tenant Management Endpoints

Create apps/api/src/tenancy/tenants.controller.ts:

@ApiTags('Tenants')
@Controller('api/v1/tenants')
@UseGuards(SessionGuard)
export class TenantsController {
  constructor(
    private readonly tenantsService: TenantsService,
    private readonly authService: AuthService
  ) {}

  @Get()
  @UseGuards(SuperAdminGuard)
  @ApiOperation({ summary: 'List all tenants (Super Admin only)' })
  async listTenants(): Promise<TenantDto[]> {
    return this.tenantsService.findAll();
  }

  @Post()
  @UseGuards(SuperAdminGuard)
  @ApiOperation({ summary: 'Create a new tenant (Super Admin only)' })
  async createTenant(@Body() dto: CreateTenantDto): Promise<TenantDto> {
    return this.tenantsService.create(dto);
  }

  @Get(':id')
  @UseGuards(SuperAdminGuard)
  @ApiOperation({ summary: 'Get tenant details (Super Admin only)' })
  async getTenant(@Param('id') id: string): Promise<TenantDto> {
    return this.tenantsService.findById(id);
  }

  @Post('switch')
  @UseGuards(SuperAdminGuard)
  @ApiOperation({ summary: 'Switch active tenant (Super Admin only)' })
  async switchTenant(
    @Body() dto: SwitchTenantDto,
    @CurrentUser() user: UserSession,
    @Res({ passthrough: true }) res: Response
  ): Promise<{ success: boolean; activeTenantId: string }> {
    // Update session with new active tenant
    await this.authService.updateActiveTenant(user.userId, dto.tenantId, res);
    return { success: true, activeTenantId: dto.tenantId };
  }

  @Get('current')
  @ApiOperation({ summary: 'Get current active tenant' })
  async getCurrentTenant(@CurrentUser() user: UserSession): Promise<TenantDto> {
    const tenantId = user.isSuperAdmin ? user.activeTenantId : user.tenantId;
    return this.tenantsService.findById(tenantId);
  }
}

🔒 Repository Tenant Enforcement

Pattern for All Repositories

Every repository method that operates on tenant-owned data must follow this pattern:

@Injectable()
export class OrdersRepository {
  constructor(private readonly prisma: PrismaService) {}

  // ✅ CORRECT: tenantId required, used in where clause
  async findById(tenantId: string, id: string): Promise<Order | null> {
    return this.prisma.order.findFirst({
      where: { id, tenantId }, // Always include tenantId
    });
  }

  // ✅ CORRECT: tenantId in compound where for update
  async updateStatus(tenantId: string, id: string, status: OrderStatus): Promise<Order> {
    return this.prisma.order.update({
      where: { id, tenantId }, // Ensures tenant isolation
      data: { status, updatedAt: new Date() },
    });
  }

  // ✅ CORRECT: findMany always filtered by tenant
  async findAll(tenantId: string, options: FindOrdersOptions): Promise<Order[]> {
    return this.prisma.order.findMany({
      where: {
        tenantId, // Always filter by tenant
        ...options.filters,
      },
      orderBy: options.orderBy,
      take: options.limit,
      skip: options.offset,
    });
  }

  // ✅ CORRECT: delete also requires tenant verification
  async delete(tenantId: string, id: string): Promise<void> {
    await this.prisma.order.deleteMany({
      where: { id, tenantId },
    });
  }
}

Repositories to Update

Repository Methods to Update
OrdersRepository findById, updateStatus, update, updateFulfillment, delete
PrintJobsRepository findById, findByLineItemId, findByOrderId, update, delete
ShipmentsRepository findById, findByOrderId, update, updateByOrderId, delete
LineItemsRepository All methods (if exists)
EventLogRepository All methods
ProductMappingRepository All methods

📦 DTO Updates

Add tenantId to All Domain DTOs

Update libs/domain-contracts/src/lib/types.ts:

export interface OrderDto {
  id: string;
  tenantId: string; // ← ADD THIS
  shopifyOrderId: string;
  shopifyOrderNumber: string;
  status: OrderStatusType;
  customerName: string;
  customerEmail: string | null;
  shippingAddress: unknown;
  trackingNumber: string | null;
  trackingUrl: string | null;
  shopifyFulfillmentId: string | null;
  totalParts: number;
  completedParts: number;
  createdAt: Date;
  updatedAt: Date;
}

export interface LineItemDto {
  id: string;
  tenantId: string; // ← ADD THIS
  orderId: string;
  shopifyLineItemId: string;
  productSku: string;
  productName: string;
  variantTitle: string | null;
  quantity: number;
  status: LineItemStatusType;
  totalParts: number;
  completedParts: number;
}

export interface PrintJobDto {
  id: string;
  tenantId: string; // ← ADD THIS
  lineItemId: string;
  simplyPrintJobId: string | null;
  status: PrintJobStatusType;
  printerId: string | null;
  printerName: string | null;
  fileUrl: string | null;
  fileName: string | null;
  progress: number;
  errorMessage: string | null;
  startedAt: Date | null;
  completedAt: Date | null;
}

export interface ShipmentDto {
  id: string;
  tenantId: string; // ← ADD THIS
  orderId: string;
  sendcloudParcelId: string | null;
  carrier: string | null;
  trackingNumber: string | null;
  trackingUrl: string | null;
  labelUrl: string | null;
  status: ShipmentStatusType;
  createdAt: Date;
  updatedAt: Date;
}

🌐 Webhook Multi-Tenant Routing

Shopify Webhook Tenant Resolution

Update shopify-webhook.service.ts:

@Injectable()
export class ShopifyWebhookService {
  constructor(
    private readonly shopRepository: ShopifyShopRepository,
    private readonly ordersService: OrdersService,
    private readonly logger: Logger
  ) {}

  async handleOrderCreated(
    shopDomain: string, // From x-shopify-shop-domain header
    payload: ShopifyOrderPayload
  ): Promise<void> {
    // Resolve tenant from shop domain
    const shop = await this.shopRepository.findByDomain(shopDomain);

    if (!shop) {
      this.logger.warn(`Received webhook for unknown shop: ${shopDomain}`);
      throw new NotFoundException(`Shop not found: ${shopDomain}`);
    }

    if (!shop.isActive) {
      this.logger.warn(`Received webhook for inactive shop: ${shopDomain}`);
      throw new BadRequestException(`Shop is inactive: ${shopDomain}`);
    }

    const tenantId = shop.tenantId;

    // Process order with resolved tenant context
    await this.ordersService.createFromShopify(tenantId, payload);
  }
}

Webhook Controller Update

@Controller('api/v1/webhooks/shopify')
export class ShopifyWebhookController {
  @Post('orders/create')
  async handleOrderCreated(
    @Headers('x-shopify-shop-domain') shopDomain: string,
    @Headers('x-shopify-hmac-sha256') hmac: string,
    @Body() payload: ShopifyOrderPayload
  ): Promise<void> {
    // Validate HMAC (existing logic)
    await this.validateHmac(hmac, payload);

    // Route to tenant based on shop domain
    await this.webhookService.handleOrderCreated(shopDomain, payload);
  }
}

🖥️ Frontend Implementation

1) Tenant Switcher Component

Create apps/web/src/components/tenant-switcher/tenant-switcher.tsx:

import { useState, useEffect } from 'react';
import { useAuth } from '@/auth/hooks/use-auth';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';

interface Tenant {
  id: string;
  name: string;
  slug: string;
}

export function TenantSwitcher() {
  const { user, isSuperAdmin, activeTenantId, switchTenant } = useAuth();
  const [tenants, setTenants] = useState<Tenant[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    if (isSuperAdmin) {
      fetchTenants();
    }
  }, [isSuperAdmin]);

  const fetchTenants = async () => {
    const response = await fetch('/api/v1/tenants', {
      credentials: 'include',
    });
    if (response.ok) {
      setTenants(await response.json());
    }
  };

  const handleTenantChange = async (tenantId: string) => {
    setIsLoading(true);
    try {
      await switchTenant(tenantId);
      // Refresh page to load new tenant's data
      window.location.reload();
    } finally {
      setIsLoading(false);
    }
  };

  if (!isSuperAdmin) {
    return null;
  }

  return (
    <div className="flex items-center gap-2">
      <span className="text-sm text-muted-foreground">Tenant:</span>
      <Select value={activeTenantId} onValueChange={handleTenantChange} disabled={isLoading}>
        <SelectTrigger className="w-48">
          <SelectValue placeholder="Select tenant" />
        </SelectTrigger>
        <SelectContent>
          {tenants.map((tenant) => (
            <SelectItem key={tenant.id} value={tenant.id}>
              {tenant.name}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  );
}

2) Auth Context Update

Update apps/web/src/auth/auth-context.tsx:

interface AuthContextValue {
  user: User | null;
  isAuthenticated: boolean;
  isSuperAdmin: boolean;
  activeTenantId: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
  switchTenant: (tenantId: string) => Promise<void>;
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [activeTenantId, setActiveTenantId] = useState<string | null>(null);

  const switchTenant = async (tenantId: string) => {
    const response = await fetch('/api/v1/tenants/switch', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ tenantId }),
    });

    if (response.ok) {
      const data = await response.json();
      setActiveTenantId(data.activeTenantId);
    }
  };

  const value: AuthContextValue = {
    user,
    isAuthenticated: !!user,
    isSuperAdmin: user?.isSuperAdmin ?? false,
    activeTenantId,
    login,
    logout,
    switchTenant,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

3) Header with Tenant Switcher

Update header component to show tenant switcher for super admins:

import { TenantSwitcher } from '@/components/tenant-switcher/tenant-switcher';
import { useAuth } from '@/auth/hooks/use-auth';

export function Header() {
  const { user, isSuperAdmin } = useAuth();

  return (
    <header className="border-b">
      <div className="flex items-center justify-between px-6 py-4">
        <div className="flex items-center gap-4">
          <Logo />
          {isSuperAdmin && <TenantSwitcher />}
        </div>
        <div className="flex items-center gap-4">
          <UserMenu user={user} />
        </div>
      </div>
    </header>
  );
}

🧪 Testing Requirements

Backend Unit Tests

Add/extend tests for:

  • Super Admin guard
  • Allows access for super admin users
  • Denies access for regular users
  • Denies access for unauthenticated requests

  • Tenant context service

  • Resolves activeTenantId for super admin
  • Resolves tenantId for regular users
  • Throws for unauthenticated requests

  • Tenant switching

  • Super admin can switch to any tenant
  • Regular user cannot switch tenants
  • Session is updated with new active tenant

  • Repository tenant isolation

  • findById only returns records matching tenantId
  • update only modifies records matching tenantId
  • update throws if record doesn't belong to tenant
  • delete only removes records matching tenantId

  • Webhook tenant routing

  • Shop domain correctly resolves to tenant
  • Unknown shop domain throws error
  • Inactive shop throws error

Frontend Tests

  • Tenant switcher
  • Only renders for super admin users
  • Displays all available tenants
  • Triggers switch on selection
  • Shows loading state during switch

  • Auth context

  • Exposes isSuperAdmin correctly
  • Exposes activeTenantId correctly
  • switchTenant updates state

Acceptance Tests (Playwright)

Add scenarios:

  • Super admin can log in and see tenant switcher
  • Super admin can switch between tenants
  • Regular user cannot see tenant switcher
  • Data isolation: switching tenants shows different orders
  • Super admin can create a new tenant
  • Super admin can view tenant list

✅ Validation Checklist

Infrastructure

  • pnpm nx build api succeeds
  • pnpm nx build web succeeds
  • pnpm lint passes
  • No TypeScript errors
  • Prisma migration runs cleanly

Multi-Tenancy

  • All DTOs include tenantId
  • All repository methods require tenantId parameter
  • All update/delete operations use compound where with tenantId
  • TenantContext correctly resolves for super admin vs regular user
  • Webhooks route to correct tenant based on shop domain

Super Admin

  • Super admin user can be created (seed or API)
  • Super admin can list all tenants
  • Super admin can switch active tenant
  • Super admin can access any tenant's data
  • Regular users cannot access super admin endpoints
  • Tenant switcher only visible to super admins

Data Isolation

  • Regular user can only see their tenant's orders
  • Regular user cannot modify other tenant's data
  • API returns 404 (not 403) for cross-tenant access attempts
  • Audit logs include correct tenantId

🎬 Execution Order

Phase 1: Database & Models (4 hours)

  1. Add isSuperAdmin to User model in Prisma schema
  2. Create migration
  3. Update seed to create a super admin user
  4. Add tenantId to all domain DTOs in libs/domain-contracts

Phase 2: Repository Tenant Enforcement (8 hours)

  1. Update OrdersRepository - add tenantId to all methods
  2. Update PrintJobsRepository - add tenantId to all methods
  3. Update ShipmentsRepository - add tenantId to all methods
  4. Update LineItemsRepository - add tenantId to all methods
  5. Update EventLogRepository - add tenantId to all methods
  6. Update ProductMappingRepository - add tenantId to all methods
  7. Write unit tests for tenant isolation in each repository

Phase 3: Service Layer Updates (6 hours)

  1. Update TenantContextService to support super admin context
  2. Update all services to get tenantId from TenantContext
  3. Update services to pass tenantId to repository calls
  4. Update webhook services for multi-tenant routing

Phase 4: Super Admin API (6 hours)

  1. Create SuperAdminGuard
  2. Create TenantsController with CRUD endpoints
  3. Create TenantsService and TenantsRepository
  4. Implement tenant switching (session update)
  5. Write tests for super admin endpoints

Phase 5: Frontend Updates (8 hours)

  1. Update AuthContext with isSuperAdmin and switchTenant
  2. Create TenantSwitcher component
  3. Update header to show tenant switcher
  4. Create tenant management pages (list, create, detail)
  5. Update router with tenant management routes
  6. Write frontend tests

Phase 6: Testing & Validation (4 hours)

  1. Run full test suite
  2. Add acceptance tests for super admin flows
  3. Manual testing of tenant isolation
  4. Documentation updates

📊 Expected Output

Verification Commands

# Database
pnpm prisma migrate dev
pnpm prisma db seed

# Build
pnpm nx build api
pnpm nx build web

# Lint + tests
pnpm lint
pnpm test
pnpm nx test acceptance-tests

# Create super admin (after seed)
# Super admin should exist from seed, or create via:
# UPDATE "User" SET "isSuperAdmin" = true WHERE email = 'admin@forma3d.be';

Success Criteria

  • Super admin can log in and switch between tenants
  • Each tenant's data is completely isolated
  • Regular users cannot see other tenants' data
  • Webhooks correctly route to the appropriate tenant
  • All existing functionality continues to work
  • No cross-tenant data leakage under any circumstances

📝 Documentation Updates

Update docs to reflect:

  • How to create a super admin user
  • How super admin tenant switching works
  • Multi-tenant data isolation architecture
  • Webhook routing for multi-tenant
  • How to onboard a new tenant

🚫 Constraints and Rules

MUST DO

  • Add tenantId to ALL domain DTOs
  • Use compound where clauses for ALL update/delete operations
  • Enforce tenant context in ALL repository methods
  • Super admin functionality is behind proper authorization
  • All tests must pass including new tenant isolation tests

MUST NOT

  • Allow repository methods to work without explicit tenantId
  • Allow cross-tenant access for regular users
  • Store super admin privileges in tenant-level roles
  • Skip tenant validation for "convenience"
  • Use any or ts-ignore to bypass type checking

END OF PROMPT

This prompt implements full multi-tenancy with super admin capabilities for Forma3D.Connect. The AI should implement complete tenant isolation, super admin tenant switching, and multi-tenant webhook routing while maintaining all existing functionality. Security and data isolation are paramount - no cross-tenant access should be possible for regular users.