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
- Single Database: All tenants share the same MongoDB database
- Data Isolation: Every business collection includes a
tenantIdfield - Query Scoping: All queries are automatically scoped by
tenantId - 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
- Extracts user from request (set by authentication middleware)
- For Inleed users:
- Can override tenantId via query param
?tenantId=xxxor headerX-Tenant-Id - If no override, uses their own tenantId (if any)
- Can override tenantId via query param
- For Client users:
- Always uses their assigned tenantId
- Cannot access other tenants
- Attaches
req.tenantIdandreq.isInleedfor 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:
- Add tenantId field to interface
// Example: IProduct interface
export interface IProduct extends Document {
tenantId: Types.ObjectId; // Add this
// ... other fields
}
- 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
});
- 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 });
- 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
});
}
- 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
- Never trust client-provided tenantId: Always use
req.tenantIdset by middleware - Always scope queries: Every query must include tenantId filter
- Validate ownership: Check tenant ownership before updates/deletes
- 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
- Login as tenant user
- Try to access data from another tenant
- 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
- Update Remaining Models: Add tenantId to all business models
- Frontend Implementation: Build tenant selection and context management
- Data Migration: Create migration script for existing data
- Testing: Comprehensive testing of tenant isolation
- Documentation: Update API documentation with tenant requirements
- Monitoring: Add logging for cross-tenant access attempts
Support
For questions or issues with the multi-tenant implementation, contact the development team.