8.3 KiB
8.3 KiB
Multi-Tenant Migration - Implementation Summary
Overview
The platform has been migrated from a single-database multi-tenant architecture to a one database per tenant architecture with subdomain-based tenant identification.
Architecture Changes
Database Layer
- Central Database (Prisma): Stores tenant metadata, domain mappings, encrypted credentials
- Tenant Databases (Knex.js + Objection.js): One MySQL database per tenant with isolated data
Tenant Identification
- Before:
x-tenant-idheader - After: Subdomain extraction from hostname (e.g.,
acme.routebox.co→ tenantacme) - Fallback:
x-tenant-idheader for local development
Technology Stack
- Central DB ORM: Prisma 5.8.0
- Tenant DB Migration: Knex.js 3.x
- Tenant DB ORM: Objection.js 3.x
- Database Driver: mysql2
File Structure
Backend - Tenant Management
src/tenant/
├── tenant-database.service.ts # Knex connection manager with encryption
├── tenant-provisioning.service.ts # Create/destroy tenant databases
├── tenant-provisioning.controller.ts # API for tenant provisioning
├── tenant.middleware.ts # Subdomain extraction & tenant injection
└── tenant.module.ts # Module configuration
migrations/tenant/ # Knex migrations for tenant databases
├── 20250126000001_create_users_and_rbac.js
├── 20250126000002_create_object_definitions.js
├── 20250126000003_create_apps.js
└── 20250126000004_create_standard_objects.js
Backend - Models (Objection.js)
src/models/
├── base.model.ts # Base model with timestamps
├── user.model.ts # User with roles
├── role.model.ts # Role with permissions
├── permission.model.ts # Permission
├── user-role.model.ts # User-Role join table
├── role-permission.model.ts # Role-Permission join table
├── object-definition.model.ts # Dynamic object metadata
├── field-definition.model.ts # Field metadata
├── app.model.ts # Application
├── app-page.model.ts # Application pages
└── account.model.ts # Standard Account object
Backend - Schema Management
src/object/
├── schema-management.service.ts # Dynamic table creation from ObjectDefinitions
└── object.service.ts # Object CRUD operations (needs migration)
Central Database Schema (Prisma)
prisma/
├── schema-central.prisma # Tenant, Domain models
└── migrations/ # Will be created when generating
Setup Instructions
1. Environment Configuration
Copy .env.example to .env and configure:
cd /root/neo/backend
cp .env.example .env
Generate encryption key:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Update .env with the generated key and database URLs:
CENTRAL_DATABASE_URL="mysql://user:password@platform-db:3306/central_platform"
ENCRYPTION_KEY="<generated-32-byte-hex-key>"
DB_ROOT_USER="root"
DB_ROOT_PASSWORD="root"
2. Central Database Setup
Generate Prisma client and run migrations:
cd /root/neo/backend
npx prisma generate --schema=./prisma/schema-central.prisma
npx prisma migrate dev --schema=./prisma/schema-central.prisma --name init
3. Tenant Provisioning
Create a new tenant via API:
curl -X POST http://localhost:3000/setup/tenants \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corporation",
"slug": "acme",
"primaryDomain": "acme"
}'
This will:
- Create MySQL database
tenant_acme - Create database user
tenant_acme_user - Run all Knex migrations on the new database
- Seed default roles and permissions
- Store encrypted credentials in central database
- Create domain mapping (
acme→ tenant)
4. Testing Subdomain Routing
Update your hosts file or DNS to point subdomains to your server:
127.0.0.1 acme.localhost
127.0.0.1 demo.localhost
Access the application:
- Central setup:
http://localhost:3000/setup/tenants - Tenant app:
http://acme.localhost:3000/ - Different tenant:
http://demo.localhost:3000/
Migration Status
✅ Completed
- Central database schema (Tenant, Domain models)
- Knex + Objection.js installation
- TenantDatabaseService with dynamic connections
- Password encryption/decryption (AES-256-CBC)
- Base Objection.js models (User, Role, Permission, etc.)
- Knex migrations for base tenant schema
- Tenant middleware with subdomain extraction
- Tenant provisioning service (create/destroy)
- Schema management service (dynamic table creation)
🔄 Pending
- Generate Prisma client for central database
- Run Prisma migrations for central database
- Migrate AuthService from Prisma to Objection.js
- Migrate RBACService from Prisma to Objection.js
- Migrate ObjectService from Prisma to Objection.js
- Migrate AppBuilderService from Prisma to Objection.js
- Update frontend to work with subdomains
- Test tenant provisioning flow
- Test subdomain routing
- Test database isolation
Service Migration Guide
Example: Migrating a Service from Prisma to Objection
Before (Prisma):
async findUser(email: string) {
return this.prisma.user.findUnique({ where: { email } });
}
After (Objection + Knex):
constructor(private readonly tenantDbService: TenantDatabaseService) {}
async findUser(tenantId: string, email: string) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
return User.query(knex).findOne({ email });
}
Key Changes
- Inject
TenantDatabaseServiceinstead ofPrismaService - Get tenant Knex connection:
await this.tenantDbService.getTenantKnex(tenantId) - Use Objection models:
User.query(knex).findOne({ email }) - Pass
tenantIdto all service methods (extract from request in controller)
API Changes
Tenant Provisioning Endpoints
Create Tenant
POST /setup/tenants
Content-Type: application/json
{
"name": "Company Name",
"slug": "company-slug",
"primaryDomain": "company",
"dbHost": "platform-db", // optional
"dbPort": 3306 // optional
}
Response:
{
"tenantId": "uuid",
"dbName": "tenant_company-slug",
"dbUsername": "tenant_company-slug_user",
"dbPassword": "generated-password"
}
Delete Tenant
DELETE /setup/tenants/:tenantId
Response:
{
"success": true
}
Security Considerations
- Encryption: Tenant database passwords are encrypted with AES-256-CBC before storage
- Isolation: Each tenant has a dedicated MySQL database and user
- Credentials: Database credentials stored in central DB, never exposed to tenants
- Subdomain Validation: Middleware validates tenant exists and is active before processing requests
Troubleshooting
Connection Issues
Check tenant connection cache:
await this.tenantDbService.disconnectTenant(tenantId);
const knex = await this.tenantDbService.getTenantKnex(tenantId); // Fresh connection
Migration Issues
Run migrations manually:
cd /root/neo/backend
npx knex migrate:latest --knexfile=knexfile.js
Encryption Key Issues
If ENCRYPTION_KEY is not set, generate one:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Next Steps
-
Generate Central DB Schema
npx prisma generate --schema=./prisma/schema-central.prisma npx prisma migrate dev --schema=./prisma/schema-central.prisma -
Migrate Existing Services
- Start with
AuthService(most critical) - Then
RBACService,ObjectService,AppBuilderService - Update all controllers to extract
tenantIdfrom request
- Start with
-
Frontend Updates
- Update API calls to include subdomain
- Test cross-tenant isolation
- Update login flow to redirect to tenant subdomain
-
Testing
- Create multiple test tenants
- Verify data isolation
- Test subdomain routing
- Performance testing with multiple connections
-
Production Deployment
- Set up wildcard DNS for subdomains
- Configure SSL certificates for subdomains
- Set up database backup strategy per tenant
- Monitor connection pool usage