NestJS SDK for multi-tenant applications with automatic database routing, JWT authentication, and request-scoped tenant context management.
npm install @vritti/api-sdkNestJS SDK for multi-tenant applications with automatic database routing, JWT authentication, and request-scoped tenant context management.


- š¢ Multi-tenant Database Management: Automatic tenant routing with connection pooling
- š JWT Authentication: Built-in auth guard with refresh token validation
- š Gateway & Microservice Support: Optimized for both HTTP APIs and RabbitMQ workers
- šÆ Request-Scoped Context: Tenant information available throughout the request lifecycle
- š”ļø Decorators: @Public(), @Onboarding(), and @Tenant() for flexible access control
- ā” Zero Configuration: Auto-registers guards and interceptors
- š Unified Logging: Environment-aware logging with PII masking, correlation IDs, and multi-tenant context
``bashnpm
npm install @vritti/api-sdk @nestjs/jwt @nestjs/config @prisma/client
Quick Start
$3
For REST APIs and GraphQL gateways that serve HTTP requests:
`typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PrismaClient } from '@prisma/client';
import { AuthConfigModule, DatabaseModule } from '@vritti/api-sdk';@Module({
imports: [
// Environment configuration
ConfigModule.forRoot({ isGlobal: true }),
// Multi-tenant database (Gateway mode)
DatabaseModule.forServer({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
primaryDb: {
host: config.get('PRIMARY_DB_HOST'),
port: config.get('PRIMARY_DB_PORT'),
username: config.get('PRIMARY_DB_USERNAME'),
password: config.get('PRIMARY_DB_PASSWORD'),
database: config.get('PRIMARY_DB_DATABASE'),
},
prismaClientConstructor: PrismaClient,
}),
}),
// JWT authentication
AuthConfigModule.forRootAsync(),
],
})
export class AppModule {}
`$3
For microservices that process messages from queues:
`typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PrismaClient } from '@prisma/client';
import { AuthConfigModule, DatabaseModule } from '@vritti/api-sdk';@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
// Multi-tenant database (Microservice mode)
DatabaseModule.forMicroservice({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
prismaClientConstructor: PrismaClient,
}),
}),
AuthConfigModule.forRootAsync(),
],
})
export class AppModule {}
`Environment Variables
$3
`bash
JWT_SECRET=your-access-token-secret-key
`$3
`bash
Primary database (tenant registry)
PRIMARY_DB_HOST=localhost
PRIMARY_DB_PORT=5432
PRIMARY_DB_USERNAME=postgres
PRIMARY_DB_PASSWORD=postgres
PRIMARY_DB_DATABASE=vritti_primary
PRIMARY_DB_SCHEMA=publicOptional
JWT_REFRESH_SECRET=your-refresh-token-secret-key
PRIMARY_DB_SSL_MODE=prefer # Options: require, prefer, disable
`Usage Examples
$3
Use
@Public() to bypass authentication:`typescript
import { Controller, Post, Body } from '@nestjs/common';
import { Public } from '@vritti/api-sdk';@Controller('auth')
export class AuthController {
@Public()
@Post('login')
async login(@Body() dto: LoginDto) {
// No authentication required
return this.authService.login(dto);
}
}
`$3
Use
@Onboarding() for registration/verification flows:`typescript
import { Controller, Post, Request } from '@nestjs/common';
import { Onboarding } from '@vritti/api-sdk';@Controller('onboarding')
export class OnboardingController {
@Onboarding()
@Post('verify-email')
async verifyEmail(@Request() req) {
const userId = req.user.id; // Available from auth guard
return this.onboardingService.verifyEmail(userId);
}
}
`$3
Use
@Tenant() to inject tenant metadata:`typescript
import { Controller, Get, Post, Body } from '@nestjs/common';
import { Tenant, TenantInfo } from '@vritti/api-sdk';@Controller('users')
export class UsersController {
@Get('info')
async getTenantInfo(@Tenant() tenant: TenantInfo) {
return {
id: tenant.id,
subdomain: tenant.subdomain,
type: tenant.type, // STARTER, PROFESSIONAL, ENTERPRISE
};
}
@Post()
async createUser(
@Body() dto: CreateUserDto,
@Tenant() tenant: TenantInfo,
) {
this.logger.log(
Creating user for tenant: ${tenant.subdomain});
// Tenant-specific logic
if (tenant.type === 'ENTERPRISE') {
// Enable enterprise features
}
return this.usersService.create(dto);
}
}
`$3
Access tenant-specific database connections:
`typescript
import { Injectable } from '@nestjs/common';
import { TenantDatabaseService } from '@vritti/api-sdk';@Injectable()
export class UsersService {
constructor(
private readonly tenantDb: TenantDatabaseService,
) {}
async findAll() {
// Automatically uses tenant's database
const db = await this.tenantDb.getClient();
return db.user.findMany();
}
async create(data: CreateUserDto) {
const db = await this.tenantDb.getClient();
return db.user.create({ data });
}
}
`$3
The SDK provides base repository classes for common CRUD operations with automatic tenant scoping:
#### Primary Database Repositories
For entities in the primary/platform database (tenants, users, sessions, etc.):
`typescript
import { Injectable } from '@nestjs/common';
import { PrimaryBaseRepository, PrimaryDatabaseService } from '@vritti/api-sdk';
import { User, CreateUserDto, UpdateUserDto } from './types';@Injectable()
export class UserRepository extends PrimaryBaseRepository<
User,
CreateUserDto,
UpdateUserDto
> {
constructor(database: PrimaryDatabaseService) {
// Use model delegate pattern - type-safe with IDE autocomplete!
super(database, (prisma) => prisma.user);
}
// Add custom methods as needed
async findByEmail(email: string): Promise {
return this.model.findUnique({ where: { email } });
}
async findActiveUsers(): Promise {
return this.model.findMany({
where: { status: 'ACTIVE' },
orderBy: { createdAt: 'desc' },
});
}
}
`#### Tenant Database Repositories
For tenant-scoped entities (products, orders, customers, etc.):
`typescript
import { Injectable } from '@nestjs/common';
import { TenantBaseRepository, TenantDatabaseService } from '@vritti/api-sdk';
import { Product, CreateProductDto, UpdateProductDto } from './types';@Injectable()
export class ProductRepository extends TenantBaseRepository<
Product,
CreateProductDto,
UpdateProductDto
> {
constructor(database: TenantDatabaseService) {
// Short syntax is also supported
super(database, (p) => p.product);
}
// Custom methods for product-specific queries
async findBySku(sku: string): Promise {
return this.model.findUnique({ where: { sku } });
}
async findInStock(): Promise {
return this.model.findMany({
where: { quantity: { gt: 0 } },
});
}
}
`#### Available Base Repository Methods
Both
PrimaryBaseRepository and TenantBaseRepository provide these methods:`typescript
// Create
await repository.create(data);// Read
await repository.findById(id);
await repository.findOne({ where: { email } });
await repository.findMany({ where: { status: 'ACTIVE' } });
// Update
await repository.update(id, data);
await repository.updateMany({ status: 'PENDING' }, { status: 'ACTIVE' });
// Delete
await repository.delete(id);
await repository.deleteMany({ status: 'INACTIVE' });
// Count & Exists
await repository.count({ status: 'ACTIVE' });
await repository.exists({ email: 'user@example.com' });
`#### Benefits of the Model Delegate Pattern
`typescript
// ā
Type-safe with IDE autocomplete
super(database, (prisma) => prisma.user);// ā
Refactor-friendly - TypeScript errors if model name changes
super(database, (p) => p.emailVerification);
// ā
Works with complex model names
super(database, (p) => p.inventoryItem);
// ā
No hardcoded strings
// ā Old way: super(database, 'user') // Error-prone!
`Architecture
$3
How it works:
1. HTTP request arrives with tenant identifier (subdomain or
x-tenant-id header)
2. TenantContextInterceptor extracts tenant identifier
3. PrimaryDatabaseService queries tenant registry for configuration
4. VrittiAuthGuard validates JWT tokens and tenant status
5. Tenant context is available throughout the request via TenantContextServiceTenant Resolution:
- Primary: Subdomain (
acme.api.vritti.com ā acme)
- Fallback: x-tenant-id header$3
How it works:
1. RabbitMQ message arrives with embedded tenant information
2.
MessageTenantContextInterceptor extracts tenant from message payload
3. Tenant context is set in TenantContextService
4. No primary database lookup needed (tenant info comes from gateway)Expected Message Format:
`typescript
{
dto: { / your data / },
tenant: {
id: 'tenant-uuid',
subdomain: 'acme',
type: 'ENTERPRISE',
databaseHost: 'tenant-db.aws.com',
databaseName: 'acme_db',
// ... other config
}
}
`Unified Logging
The SDK provides a comprehensive logging system with built-in support for correlation IDs, HTTP logging, and multi-tenant context tracking. Choose between NestJS default logger or Winston with environment-based presets.
$3
- šÆ Dual Provider Support: Switch between NestJS default Logger and Winston
- š Environment Presets: Pre-configured settings for development, staging, production, and test
- š Correlation IDs: Track requests across services with automatic ID generation and propagation
- š¢ Multi-Tenant Context: Automatically includes tenant and user IDs in logs
- š File Logging: Automatic file rotation with configurable retention
- š HTTP Request/Response Logging: Automatic logging of all HTTP traffic
- ā” Zero Configuration: Works out of the box with sensible defaults
$3
Import the
LoggerModule in your application:`typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { LoggerModule } from '@vritti/api-sdk';@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
// Option 1: Simple configuration with environment preset
LoggerModule.forRoot({
environment: 'development', // Required: development, staging, production, test
appName: 'my-service',
}),
// Option 2: Dynamic configuration with ConfigService
LoggerModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
environment: config.get('NODE_ENV', 'development'),
appName: config.get('APP_NAME'),
provider: config.get('LOG_PROVIDER'), // 'default' or 'winston'
level: config.get('LOG_LEVEL'), // Optional override
format: config.get('LOG_FORMAT'), // Optional override
enableFileLogger: config.get('LOG_TO_FILE') === 'true',
enableHttpLogger: true,
httpLogger: {
enableRequestLog: true,
enableResponseLog: true,
slowRequestThreshold: 3000,
},
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
`Inject and use the
LoggerService:`typescript
import { Injectable } from '@nestjs/common';
import { LoggerService } from '@vritti/api-sdk';@Injectable()
export class UsersService {
constructor(private readonly logger: LoggerService) {}
async createUser(data: CreateUserDto) {
this.logger.log('Creating new user', 'UsersService');
try {
const user = await this.userRepository.create(data);
this.logger.log('User created successfully', { userId: user.id });
return user;
} catch (error) {
this.logger.error('Failed to create user', error.stack, 'UsersService');
throw error;
}
}
}
`$3
The logger module provides pre-configured settings based on environment:
| Environment | Provider | Level | Format | File Logging | HTTP Logging |
|------------|----------|-------|--------|--------------|--------------|
| development | winston | debug | text | No | Yes (verbose) |
| staging | winston | log | json | Yes | Yes |
| production | winston | warn | json | Yes | Limited |
| test | winston | error | json | No | No |
$3
Choose between NestJS default Logger or Winston:
`typescript
// Use NestJS default Logger
LoggerModule.forRoot({
environment: 'development',
provider: 'default', // Simple, built-in NestJS logger
})// Use Winston (default)
LoggerModule.forRoot({
environment: 'production',
provider: 'winston', // Advanced features, file logging, etc.
})
`Environment Variable:
`bash
In .env file
LOG_PROVIDER=default # or 'winston'
`Important: When using
LOG_PROVIDER=default, update your main.ts to avoid circular references:`typescript
async function bootstrap() {
const logProvider = process.env.LOG_PROVIDER || 'winston';
const useBuiltInLogger = logProvider === 'default'; const app = await NestFactory.create(
AppModule,
new FastifyAdapter(),
useBuiltInLogger ? {} : {
logger: new LoggerService({
environment: process.env.NODE_ENV
})
},
);
// Only replace logger when using Winston
if (!useBuiltInLogger) {
const appLogger = app.get(LoggerService);
app.useLogger(appLogger);
}
// ... rest of bootstrap
}
`$3
HTTP logging is automatically enabled when
enableHttpLogger: true. The interceptor is registered globally:`typescript
LoggerModule.forRoot({
environment: 'development',
enableHttpLogger: true,
httpLogger: {
enableRequestLog: true, // Log incoming requests
enableResponseLog: true, // Log outgoing responses
slowRequestThreshold: 3000, // Warn on requests > 3 seconds
},
})
`Request Log Example:
`
2025-01-23T10:30:45.123Z INFO [abc123] [HTTP] ā POST /api/users
`Response Log Example:
`
2025-01-23T10:30:45.456Z INFO [abc123] [HTTP] ā 201 POST /api/users (333ms)
`Slow Request Warning:
`
2025-01-23T10:30:50.789Z WARN [abc123] [HTTP] ā 200 GET /api/reports (4521ms) [SLOW]
`$3
Correlation IDs are automatically included in all logs when the middleware is registered:
`typescript
// In main.ts (Fastify)
const correlationMiddleware = app.get(CorrelationIdMiddleware);
const fastifyInstance = app.getHttpAdapter().getInstance();
fastifyInstance.addHook('onRequest', async (request, reply) => {
await correlationMiddleware.onRequest(request as any, reply as any);
});
`The correlation ID appears in all logs:
`
2025-01-23T10:30:45.123Z INFO [abc123] [UsersService] Creating new user
`$3
Override preset defaults for specific needs:
`typescript
LoggerModule.forRoot({
environment: 'production', // Start with production preset
level: 'debug', // Override: use debug level
enableFileLogger: true, // Enable file logging
filePath: './logs', // Custom log directory
maxFiles: '30d', // Keep logs for 30 days
httpLogger: {
enableRequestLog: true, // Override: enable request logs in production
enableResponseLog: true,
slowRequestThreshold: 5000, // 5 seconds
},
})
`$3
Add custom metadata to enrich your logs (Winston only):
`typescript
this.logger.logWithMetadata(
'log',
'Payment processed',
{
orderId: order.id,
amount: order.total,
paymentMethod: 'credit_card',
},
'PaymentService'
);
`$3
Create context-specific loggers:
`typescript
@Injectable()
export class OrderService {
private readonly logger: LoggerService; constructor(loggerService: LoggerService) {
this.logger = loggerService.child('OrderService');
}
processOrder(orderId: string) {
this.logger.log('Processing order', { orderId });
// All logs from this logger will include context: "OrderService"
}
}
`$3
| Option | Type | Default | Description |
|--------|------|---------|-------------|
|
environment | string | Required | Environment preset: development, staging, production, test |
| provider | 'default' \| 'winston' | 'winston' | Logger implementation to use |
| appName | string | - | Application name (included in all logs) |
| level | string | Preset | Log level: error, warn, log, debug, verbose |
| format | 'text' \| 'json' | Preset | Log output format |
| enableFileLogger | boolean | Preset | Enable file-based logging |
| filePath | string | './logs' | Directory for log files |
| maxFiles | string | '14d' | Log retention period |
| enableHttpLogger | boolean | Preset | Enable HTTP request/response logging |
| httpLogger.enableRequestLog | boolean | Preset | Log incoming HTTP requests |
| httpLogger.enableResponseLog | boolean | Preset | Log outgoing HTTP responses |
| httpLogger.slowRequestThreshold | number | Preset | Threshold (ms) to warn on slow requests |API Reference
$3
####
DatabaseModule-
forServer(options): Configure for Gateway/HTTP mode
- forMicroservice(options): Configure for RabbitMQ/messaging mode####
AuthConfigModule-
forRootAsync(): Register JWT authentication with global guard$3
####
TenantDatabaseServiceAccess tenant-specific database connections.
`typescript
class TenantDatabaseService {
async getClient(): Promise
clearConnection(tenantId: string): void
}
`####
PrimaryDatabaseServiceAccess the primary/platform database (tenant registry). Use this for cloud-api operations like managing tenants, users, sessions, etc.
`typescript
class PrimaryDatabaseService {
async getPrimaryDbClient(): Promise
async getTenantInfo(identifier: string): Promise
}
`Example:
`typescript
@Injectable()
export class TenantRepository {
constructor(private readonly database: PrimaryDatabaseService) {} async findAll() {
const prisma = await this.database.getPrimaryDbClient();
return prisma.tenant.findMany();
}
}
`####
TenantContextServiceManage request-scoped tenant context.
`typescript
class TenantContextService {
getTenant(): TenantInfo
setTenant(tenant: TenantInfo): void
hasTenant(): boolean
clearTenant(): void
}
`$3
####
@Public()Bypass authentication on specific endpoints.
####
@Onboarding()Accept only onboarding tokens (for registration/verification flows).
####
@Tenant()Inject tenant metadata into controller methods.
$3
####
TenantInfo`typescript
interface TenantInfo {
id: string;
subdomain: string;
type: 'STARTER' | 'PROFESSIONAL' | 'ENTERPRISE';
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
databaseHost: string;
databasePort?: number;
databaseName: string;
databaseUsername: string;
databasePassword: string;
databaseSchema?: string;
sslMode?: 'require' | 'prefer' | 'disable';
}
`####
DatabaseModuleOptions`typescript
interface DatabaseModuleOptions {
// Gateway mode only
primaryDb?: {
host: string;
port?: number;
username: string;
password: string;
database: string;
schema?: string;
sslMode?: 'require' | 'prefer' | 'disable';
}; // Required for both modes
prismaClientConstructor: any;
// Optional
connectionCacheTTL?: number; // Default: 300000 (5 minutes)
maxConnections?: number; // Default: 10
}
`Development
$3
- Node.js 18+
- Yarn
- PostgreSQL (for testing)
$3
`bash
git clone https://github.com/vritti-ai-platforms/api-sdk.git
cd api-sdk
yarn install
`$3
-
yarn dev - Run in watch mode (includes type checking before start)
- yarn build - Build for production (includes type checking)
- yarn type-check - TypeScript type checking only
- yarn test - Run tests
- yarn test:watch - Run tests in watch mode
- yarn lint - Lint source files
- yarn format - Format code with Prettier
- yarn clean - Remove build artifactsNote on Type Checking:
- The
build and dev scripts now include automatic type checking via tsc --noEmit
- This ensures type errors are caught early in development
- The type-check script is still available for standalone type checking
- This follows the monorepo convention used in quantum-ui, web-nexus, and auth-microfrontend$3
The build process includes the following steps:
1. Type Checking:
tsc --noEmit validates TypeScript types without emitting files
2. Bundling: tsup bundles the code using the configuration in tsup.config.ts
3. Declaration Files: Generated automatically by tsup with source mapsIf type checking fails, the build will not proceed. This ensures published packages are type-safe.
$3
`
api-sdk/
āāā src/
ā āāā auth/ # Authentication module
ā ā āāā guards/ # VrittiAuthGuard
ā ā āāā decorators/ # @Public, @Onboarding
ā ā āāā auth-config.module.ts
ā āāā database/ # Database module
ā ā āāā services/ # Database services
ā ā āāā interceptors/ # Tenant context interceptors
ā ā āāā decorators/ # @Tenant
ā ā āāā interfaces/ # TypeScript interfaces
ā ā āāā database.module.ts
ā āāā request/ # Request utilities (internal)
ā āāā index.ts # Public API exports
āāā dist/ # Build output
āāā package.json
`Best Practices
$3
Always use
ConfigService and validate environment variables at startup:`typescript
import { plainToClass } from 'class-transformer';
import { IsString, IsNumber, validateSync } from 'class-validator';class EnvironmentVariables {
@IsString()
JWT_SECRET: string;
@IsString()
PRIMARY_DB_HOST: string;
@IsNumber()
PRIMARY_DB_PORT: number;
}
export function validate(config: Record) {
const validatedConfig = plainToClass(EnvironmentVariables, config, {
enableImplicitConversion: true,
});
const errors = validateSync(validatedConfig, {
skipMissingProperties: false,
});
if (errors.length > 0) {
throw new Error(errors.toString());
}
return validatedConfig;
}
`$3
Let the SDK manage connection pooling. Don't create custom Prisma instances:
`typescript
// ā
Good
@Injectable()
export class UsersService {
constructor(private readonly tenantDb: TenantDatabaseService) {} async findAll() {
const db = await this.tenantDb.getClient();
return db.user.findMany();
}
}
// ā Bad - Don't do this
@Injectable()
export class UsersService {
private prisma = new PrismaClient(); // ā Breaks multi-tenancy
}
`$3
Always use
@Tenant() decorator instead of manually accessing TenantContextService:`typescript
// ā
Good
@Get('info')
async getInfo(@Tenant() tenant: TenantInfo) {
return { subdomain: tenant.subdomain };
}// ā Bad - Avoid manual service injection
@Get('info')
async getInfo() {
const tenant = this.tenantContext.getTenant(); // ā Unnecessary
}
`Troubleshooting
$3
Cause: DatabaseModule not imported or registered incorrectly.
Solution: Ensure
DatabaseModule.forServer() or forMicroservice() is imported in your module.$3
Cause: Missing
JWT_SECRET environment variable.Solution: Add
JWT_SECRET to your .env file.$3
Cause: Request missing subdomain and
x-tenant-id header.Solution: Ensure requests include tenant identifier:
- Use subdomain:
https://acme.api.vritti.com
- Or add header: x-tenant-id: acme$3
Cause: Too many concurrent tenants or connections not released.
Solution: Increase
maxConnections in DatabaseModule options:`typescript
DatabaseModule.forServer({
useFactory: () => ({
// ...
maxConnections: 20, // Increase from default 10
}),
})
`Migration Guide
$3
If you're migrating from a manual setup:
1. Remove manual interceptor registrations
2. Remove manual guard registrations
3. Replace custom tenant context with
@Tenant() decorator
4. Update imports to use SDK exportsBefore:
`typescript
@Module({
imports: [RequestModule],
providers: [
{ provide: APP_GUARD, useClass: VrittiAuthGuard },
{ provide: APP_INTERCEPTOR, useClass: TenantContextInterceptor },
],
})
`After:
`typescript
@Module({
imports: [
DatabaseModule.forServer({ / config / }),
AuthConfigModule.forRootAsync(),
],
})
`Contributing
Contributions are welcome! Please follow these steps:
1. Fork the repository
2. Create a feature branch:
git checkout -b feature/my-feature
3. Make your changes
4. Run tests and linting: yarn test && yarn lint
5. Commit your changes: git commit -am 'Add new feature'
6. Push to the branch: git push origin feature/my-feature`MIT Ā© Shashank Raju
Shashank Raju
- Email: shashank@vrittiai.com
- GitHub: @vritti-ai-platforms