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:
- Multiple active tenants with complete data isolation
- Super Admin role that can view and manage all tenants
- Tenant switching for super admins after login
- Per-tenant integrations (Shopify shops, SimplyPrint accounts, Sendcloud credentials)
- 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
tenantIdfor 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:
Tenanttable with a default tenant seededtenantIdforeign keys on all tenant-owned entities (Order, PrintJob, Shipment, etc.)TenantContextServicethat resolves tenant from authenticated userUsertable with roles and permissions- Local user authentication with session cookies
- Permission-based access control via
@RequirePermissionsdecorator
What's Missing (Identified Issues)¶
- DTOs missing
tenantId OrderDto,PrintJobDto,ShipmentDto,LineItemDtoinlibs/domain-contractsdon't includetenantId-
This caused the fulfillment service to need the full Prisma
Orderinstead of the lightweight DTO -
Update operations without tenant filtering
OrdersRepository.updateStatus(),update(),updateFulfillment()usewhere: { id }withouttenantIdPrintJobsRepository.update()same issueShipmentsRepository.update(),updateByOrderId()same issue-
Security risk: These can modify any tenant's data if the record ID is known
-
No Super Admin role
- Current roles (
admin,operator,viewer) are tenant-scoped -
No mechanism for cross-tenant access
-
No tenant switching UI
- Users are locked to their assigned tenant
-
No way to view or switch between tenants
-
Services don't consistently use TenantContext
- Some services access repositories without passing
tenantId - Repositories default to
DEFAULT_TENANT_IDwhentenantIdomitted
🛠️ 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
adminpermissions 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
isSuperAdminboolean flag to theUsermodel - 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
tenantIdas a parameter (not optional) - Include
tenantIdin thewhereclause 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-domainheader →ShopifyShop.shopDomain→tenantId - 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
findByIdonly returns records matching tenantIdupdateonly modifies records matching tenantIdupdatethrows if record doesn't belong to tenant-
deleteonly 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 apisucceeds -
pnpm nx build websucceeds -
pnpm lintpasses - No TypeScript errors
- Prisma migration runs cleanly
Multi-Tenancy¶
- All DTOs include
tenantId - All repository methods require
tenantIdparameter - 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)¶
- Add
isSuperAdminto User model in Prisma schema - Create migration
- Update seed to create a super admin user
- Add
tenantIdto all domain DTOs inlibs/domain-contracts
Phase 2: Repository Tenant Enforcement (8 hours)¶
- Update
OrdersRepository- add tenantId to all methods - Update
PrintJobsRepository- add tenantId to all methods - Update
ShipmentsRepository- add tenantId to all methods - Update
LineItemsRepository- add tenantId to all methods - Update
EventLogRepository- add tenantId to all methods - Update
ProductMappingRepository- add tenantId to all methods - Write unit tests for tenant isolation in each repository
Phase 3: Service Layer Updates (6 hours)¶
- Update
TenantContextServiceto support super admin context - Update all services to get tenantId from TenantContext
- Update services to pass tenantId to repository calls
- Update webhook services for multi-tenant routing
Phase 4: Super Admin API (6 hours)¶
- Create
SuperAdminGuard - Create
TenantsControllerwith CRUD endpoints - Create
TenantsServiceandTenantsRepository - Implement tenant switching (session update)
- Write tests for super admin endpoints
Phase 5: Frontend Updates (8 hours)¶
- Update
AuthContextwith isSuperAdmin and switchTenant - Create
TenantSwitchercomponent - Update header to show tenant switcher
- Create tenant management pages (list, create, detail)
- Update router with tenant management routes
- Write frontend tests
Phase 6: Testing & Validation (4 hours)¶
- Run full test suite
- Add acceptance tests for super admin flows
- Manual testing of tenant isolation
- 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
tenantIdto 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
anyorts-ignoreto 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.