Aller au contenu principal

Multi-Tenant Architecture Migration Guide

Overview

This document describes the multi-tenant architecture implemented for the Baldr platform. The system now supports multiple clients (tenants) with complete data isolation while maintaining a single codebase, API, and database.

Architecture Overview

Core Principles

  1. Single Database: All tenants share the same MongoDB database
  2. Data Isolation: Every business collection includes a tenantId field
  3. Query Scoping: All queries are automatically scoped by tenantId
  4. User Types:
    • Inleed Users: Internal administrators who can manage multiple tenants
    • Client Users: Regular users who belong to one specific tenant

API Changes

1. New Tenant Model

Location: BaldrTs/src/models/tenant.model.ts

{
name: string; // Tenant display name
slug: string; // Unique URL-friendly identifier
status: TenantStatus; // active | suspended | inactive
features: ObjectId[]; // Module IDs this tenant has access to
contactEmail?: string;
metadata?: {
companyName?: string;
contactPerson?: string;
phone?: string;
address?: string;
customDomain?: string;
plan?: string;
};
}

2. Updated Credential Model

New fields added to ICredential:

{
tenantId?: ObjectId; // Reference to tenant (required for client users)
isInleed: boolean; // true for internal admins, false for clients
// ... existing fields
}

3. Authentication Response

The login endpoint now returns tenant information:

POST /api/credential/login
Response: {
success: boolean;
message: string;
token: string;
user: {
_id: string;
userName: string;
email: string;
role: string;
tenantId?: string; // Present for client users
isInleed: boolean; // Determines user type
}
}

4. Tenant Isolation Middleware

Location: BaldrTs/src/middlewares/tenantIsolation.middleware.ts

Usage Pattern

import { authenticate } from '../middlewares/authentication.middleware';
import { tenantIsolation, requireTenant } from '../middlewares/tenantIsolation.middleware';

// For routes that require tenant context
router.get('/products', authenticate, tenantIsolation, requireTenant, productController.getAll);

// For routes where tenant is optional (e.g., Inleed admin operations)
router.get('/tenants', authenticate, tenantIsolation, tenantController.getAll);

How It Works

  1. Extracts user from request (set by authentication middleware)
  2. For Inleed users:
    • Can override tenantId via query param ?tenantId=xxx or header X-Tenant-Id
    • If no override, uses their own tenantId (if any)
  3. For Client users:
    • Always uses their assigned tenantId
    • Cannot access other tenants
  4. Attaches req.tenantId and req.isInleed for use in controllers

5. Tenant Management Endpoints

All endpoints require authentication and are restricted to Inleed users (except GET /:id and /stats which allow tenant members to view their own data).

POST   /api/tenant              Create tenant (Inleed only)
GET /api/tenant List all tenants with pagination (Inleed only)
GET /api/tenant/:id Get tenant by ID
PUT /api/tenant/:id Update tenant (Inleed only)
DELETE /api/tenant/:id Delete tenant (Inleed only)
GET /api/tenant/:id/stats Get tenant statistics

List Tenants Example

GET /api/tenant?page=1&limit=20&search=acme&status=active

Response: {
items: ITenant[];
total: number;
page: number;
limit: number;
totalPages: number;
}

Migrating Existing Models

Step-by-Step Guide

For every business model that stores tenant-specific data:

  1. Add tenantId field to interface
// Example: IProduct interface
export interface IProduct extends Document {
tenantId: Types.ObjectId; // Add this
// ... other fields
}
  1. Add tenantId to schema
const ProductSchema = new Schema({
tenantId: {
type: Schema.Types.ObjectId,
required: true,
ref: 'Tenant',
index: true, // Important for query performance
},
// ... other fields
});
  1. Add compound indexes
// Always start compound indexes with tenantId
ProductSchema.index({ tenantId: 1, createdAt: -1 });
ProductSchema.index({ tenantId: 1, slug: 1 });
ProductSchema.index({ tenantId: 1, active: 1 });
  1. Update service layer queries
// Before
const products = await Product.find({ active: true });

// After (in controller with tenant isolation middleware)
const products = await Product.find({
tenantId: req.tenantId,
active: true
});

// Or in service
async getProducts(tenantId: string) {
return await Product.find({
tenantId: new Types.ObjectId(tenantId),
active: true
});
}
  1. Update create operations
async createProduct(data: any, tenantId: string) {
const product = new Product({
...data,
tenantId: new Types.ObjectId(tenantId),
});
await product.save();
return product;
}

Models That Need Migration

Based on the current codebase, these models should be updated:

  • credential.model.ts (Already updated)
  • ⚠️ command.model.ts
  • ⚠️ page.model.ts
  • ⚠️ product.model.ts
  • ⚠️ category.model.ts
  • ⚠️ gallery.model.ts
  • ⚠️ news.model.ts
  • ⚠️ newsletter.model.ts
  • ⚠️ contact.model.ts
  • ⚠️ review.model.ts
  • ⚠️ address.model.ts
  • ⚠️ booklet.model.ts
  • ⚠️ vehicle.model.ts
  • ⚠️ room.model.ts
  • ⚠️ And all other business models...

Note: Global configuration models (like language, module) may NOT need tenantId if they're shared across all tenants.

Security Considerations

Critical Rules

  1. Never trust client-provided tenantId: Always use req.tenantId set by middleware
  2. Always scope queries: Every query must include tenantId filter
  3. Validate ownership: Check tenant ownership before updates/deletes
  4. Audit Inleed actions: Log when Inleed users access tenant data

Example: Secure Controller Pattern

async updateProduct(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const updates = req.body;

// Find product with tenant scope
const product = await Product.findOne({
_id: id,
tenantId: req.tenantId, // Ensures tenant isolation
});

if (!product) {
return next(errorService.notFound('Product not found'));
}

// Update product
Object.assign(product, updates);
await product.save();

res.json(product);
} catch (error) {
next(error);
}
}

Database Indexes

Required Indexes

For optimal performance, create these indexes:

// Credentials
db.credentials.createIndex({ tenantId: 1, email: 1 });
db.credentials.createIndex({ tenantId: 1, userName: 1 });
db.credentials.createIndex({ tenantId: 1, role: 1 });
db.credentials.createIndex({ isInleed: 1, tenantId: 1 });

// For each business collection (replace 'products' with actual collection name)
db.products.createIndex({ tenantId: 1, createdAt: -1 });
db.products.createIndex({ tenantId: 1, slug: 1 });
db.products.createIndex({ tenantId: 1, active: 1 });

// Tenants
db.tenants.createIndex({ slug: 1 }, { unique: true });
db.tenants.createIndex({ status: 1, createdAt: -1 });
db.tenants.createIndex({ name: 1 });

These indexes are automatically created when the models are loaded, but you can verify with:

mongo
use baldr
db.credentials.getIndexes()
db.tenants.getIndexes()

Testing Multi-Tenant Isolation

Create Test Tenants

curl -X POST http://localhost:3000/api/tenant \
-H "Authorization: Bearer INLEED_USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"slug": "acme",
"contactEmail": "admin@acme.com",
"status": "active"
}'

Create Tenant-Specific Users

curl -X POST http://localhost:3000/api/credential \
-H "Authorization: Bearer INLEED_USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"userName": "john.acme",
"email": "john@acme.com",
"password": "SecurePass123!",
"firstName": "John",
"lastName": "Doe",
"role": "commonMaster",
"tenantId": "TENANT_ID_HERE",
"isInleed": false
}'

Verify Isolation

  1. Login as tenant user
  2. Try to access data from another tenant
  3. Should receive 403 Forbidden or empty results

Migration Checklist

Backend (API)

  • ✅ Create Tenant model and interface
  • ✅ Update Credential model with tenantId and isInleed
  • ✅ Create tenant isolation middleware
  • ✅ Update authentication to return tenant info
  • ✅ Create tenant management endpoints
  • ⚠️ Update all business models with tenantId
  • ⚠️ Update all controllers to use tenant isolation
  • ⚠️ Update all service layer queries
  • ⚠️ Add compound indexes to all collections
  • ⚠️ Test cross-tenant data isolation

Frontend (Back Office)

  • ⚠️ Update login flow to handle isInleed flag
  • ⚠️ Create tenant context provider
  • ⚠️ Build tenant selection page for Inleed users
  • ⚠️ Update routing logic (Inleed vs Client flow)
  • ⚠️ Update all API calls to include tenant context
  • ⚠️ Add tenant switching for Inleed users
  • ⚠️ Restrict tenant list access to Inleed users

Data Migration

  • ⚠️ Create default tenant for existing data
  • ⚠️ Assign tenantId to all existing credentials
  • ⚠️ Set isInleed flag for admin users
  • ⚠️ Migrate all business data to default tenant

Next Steps

  1. Update Remaining Models: Add tenantId to all business models
  2. Frontend Implementation: Build tenant selection and context management
  3. Data Migration: Create migration script for existing data
  4. Testing: Comprehensive testing of tenant isolation
  5. Documentation: Update API documentation with tenant requirements
  6. Monitoring: Add logging for cross-tenant access attempts

Support

For questions or issues with the multi-tenant implementation, contact the development team.