Step6 Multi Tenancy
✅ Step 6 — Multi-Tenancy (Auth Service Example)
🎯 Goal
- Enable per-tenant MongoDB databases (e.g.,
darmist_darmist1,darmist_darmist2). - Identify tenants by
x-tenant-idheader. - Validate tenants via tenant-service (TCP) with Redis caching.
- Use
DatabaseLibServicefrom@app/database-libfor tenant DB connections. - Attach tenant connection to
req.tenantConnection. - Show a dummy User schema in auth-service as a working test.
⚙️ 1) DatabaseLibService (per-tenant DB manager)
File: libs/database-lib/src/database-lib.service.ts
import {
Injectable,
Logger,
InternalServerErrorException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import mongoose, { Connection, ConnectOptions } from 'mongoose';
/**
* DatabaseLibService
* ------------------
* - Manages per-tenant MongoDB connections.
* - Each tenant gets a dedicated DB: {MONGO_DB_PREFIX}_{tenantName}.
* - Connections are cached for reuse.
*/
@Injectable()
export class DatabaseLibService {
private readonly logger = new Logger(DatabaseLibService.name);
private readonly connections = new Map<string, Connection>();
constructor(private readonly config: ConfigService) {}
/**
* Get or create a tenant-specific MongoDB connection.
* @param tenantName Unique tenant identifier (from x-tenant-id header)
*/
async getTenantConnection(tenantName: string): Promise<Connection> {
if (!tenantName) {
throw new InternalServerErrorException(
'Missing tenantName for DB connection',
);
}
// ✅ Return cached connection if it already exists
if (this.connections.has(tenantName)) {
return this.connections.get(tenantName)!;
}
// 🔗 Build tenant-specific DB URI
const baseUri = this.config
.get<string>('MONGO_URI', 'mongodb://localhost:27017')
.replace(///$/, '');
const prefix = this.config.get<string>('MONGO_DB_PREFIX', 'aerostitch');
const dbName = `${prefix}_${tenantName}`;
const uri = `${baseUri}/${dbName}`;
const options: ConnectOptions = {
autoCreate: true,
retryWrites: true,
w: 'majority',
};
this.logger.log(`🔌 Connecting to tenant DB "${dbName}" at ${uri}`);
try {
const conn = await mongoose.createConnection(uri, options).asPromise();
this.connections.set(tenantName, conn);
// Observability logs
conn.on('connected',
() =>
this.logger.log(`✅ Tenant DB connected: ${tenantName}`),
);
conn.on('error', (err) =>
this.logger.error(`❌ Tenant DB error (${tenantName}): ${err.message}`),
);
conn.on('disconnected',
() =>
this.logger.warn(`⚠️ Tenant DB disconnected: ${tenantName}`),
);
return conn;
} catch (err) {
this.logger.error(
`Failed to connect DB (tenant=${tenantName})`,
err.stack,
);
throw new InternalServerErrorException(
`Database connection failed for tenant "${tenantName}"`,
);
}
}
}
👉 Note: We import this service with
import { DatabaseLibService } from '@app/database-lib';
⚙️ 2) Tenant Middleware (validate tenant + attach connection)
File: libs/database-lib/src/tenant.middleware.ts
import {
Injectable,
NestMiddleware,
BadRequestException,
ForbiddenException,
Inject,
Logger,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { DatabaseLibService } from '@app/database-lib';
import { ClientProxy } from '@nestjs/microservices';
import { lastValueFrom, TimeoutError } from 'rxjs';
import { RedisLibService } from '@app/redis-lib';
/**
* TenantMiddleware
* ----------------
* - Extracts tenant name from request header.
* - Validates tenant via tenant-service (TCP).
* - Caches tenant validation in Redis.
* - Ensures tenant is ACTIVE before proceeding.
* - Attaches tenantConnection to request object.
*/
@Injectable()
export class TenantMiddleware implements NestMiddleware {
private readonly logger = new Logger(TenantMiddleware.name);
constructor(
private readonly dbService: DatabaseLibService,
@Inject('TENANT_SERVICE') private readonly tenantClient: ClientProxy,
private readonly cache: RedisLibService,
) {}
async use(req: Request, _res: Response, next: NextFunction) {
const tenantName = (req.headers['x-tenant-id'] as string)?.trim();
if (!tenantName) {
throw new BadRequestException(`Missing 'x-tenant-id' header`);
}
// 1️⃣ Try Redis cache first
let tenantRecord: any = await this.cache.get(`tenant:${tenantName}`);
// 2️⃣ Fallback to tenant-service (TCP)
if (!tenantRecord) {
try {
await this.tenantClient.connect();
tenantRecord = await lastValueFrom(
this.tenantClient.send({ cmd: 'tenant.findByName' }, tenantName),
);
if (tenantRecord?.data) {
tenantRecord = tenantRecord.data;
await this.cache.set(`tenant:${tenantName}`, tenantRecord, 300);
}
} catch (err) {
if (err instanceof TimeoutError) {
throw new BadRequestException(`Tenant validation timed out`);
}
throw new BadRequestException(`Tenant "${tenantName}" not found`);
}
}
if (!tenantRecord) {
throw new BadRequestException(`Tenant "${tenantName}" not found`);
}
// 3️⃣ Enforce ACTIVE tenants only
if (tenantRecord.status !== 'ACTIVE') {
throw new ForbiddenException(`Tenant "${tenantName}" is not active`);
}
// 4️⃣ Attach tenant-specific DB connection
const connection = await this.dbService.getTenantConnection(tenantName);
(req as any).tenantConnection = connection;
this.logger.debug(`➡️ TenantMiddleware passed (${tenantName})`);
next();
}
}
⚙️ 3) Apply Middleware in Auth Service
File: apps/auth-service/src/auth-service.module.ts
import { Module, MiddlewareConsumer } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { RedisLibModule } from '@app/redis-lib';
import { DatabaseLibService } from '@app/database-lib';
import { TenantMiddleware } from '@app/database-lib/tenant.middleware';
import { AuthTestController } from './auth-test.controller';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
RedisLibModule,
ClientsModule.registerAsync([
{
name: 'TENANT_SERVICE',
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: cfg.get<number>('TENANT_SERVICE_TCP_PORT', 4503),
},
}),
},
]),
],
controllers: [AuthTestController],
providers: [DatabaseLibService],
})
export class AuthServiceModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TenantMiddleware).forRoutes('*');
}
}
⚙️ 4) Test Controller (Dummy User Model)
File: apps/auth-service/src/auth-test.controller.ts
import { Controller, Get, Post, Body, Req } from '@nestjs/common';
import { Schema } from 'mongoose';
import { apiResponse } from '@app/common-lib';
// Define a simple User schema
const UserSchema = new Schema(
{
username: { type: String, required: true },
email: { type: String, required: true },
},
{ timestamps: true },
);
@Controller('auth-test')
export class AuthTestController {
@Post()
async create(@Req() req: any, @Body() body: any) {
const conn = req.tenantConnection;
const User = conn.model('User', UserSchema);
const created = await new User(body).save();
return apiResponse('User created (tenant scoped)', created);
}
@Get()
async findAll(@Req() req: any) {
const conn = req.tenantConnection;
const User = conn.model('User', UserSchema);
const users = await User.find().lean().exec();
return apiResponse('Users list (tenant scoped)', users);
}
}
⚙️ 5) Verify Multi-Tenant Auth Service
Start tenant-service + auth-service:
npx nest start tenant-service --watch
npx nest start auth-service --watch
Create two tenants (darmist1 & darmist2) via tenant-service and set status to ACTIVE.
Change tenant status by name
curl -X PATCH http://localhost:3501/gateway/tenants/by-name/darmist1/status \
-H "Content-Type: application/json" \
-d '{
"status": "ACTIVE"
}'
Add users in tenant darmist1:
curl -X POST http://localhost:3502/auth-test \
-H "x-tenant-id: darmist1" \
-H "Content-Type: application/json" \
-d '{"username":"alice","email":"alice@darmist1.com"}'
Add users in tenant darmist2:
curl -X POST http://localhost:3502/auth-test \
-H "x-tenant-id: darmist2" \
-H "Content-Type: application/json" \
-d '{"username":"bob","email":"bob@darmist2.com"}'
Fetch lists separately:
curl http://localhost:3502/auth-test -H "x-tenant-id: darmist1"
curl http://localhost:3502/auth-test -H "x-tenant-id: darmist2"
✅ Each tenant sees only their own users, stored in separate DBs (darmist_darmist1, darmist_darmist2).
🎉 End of Step 6
DatabaseLibService(@app/database-lib) manages tenant DBs.TenantMiddlewarevalidates tenants via tenant-service (TCP) + Redis cache.- Ensures only ACTIVE tenants can access.
- Auth-service test shows real tenant-scoped data isolation.