
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
@vritti/api-sdk
Advanced tools
NestJS SDK for multi-tenant applications with automatic database routing, JWT authentication, and request-scoped tenant context management.
NestJS SDK for multi-tenant applications with automatic database routing, JWT authentication, and request-scoped tenant context management.
@Public(), @Onboarding(), and @Tenant() for flexible access control# npm
npm install @vritti/api-sdk @nestjs/jwt @nestjs/config @prisma/client
# yarn
yarn add @vritti/api-sdk @nestjs/jwt @nestjs/config @prisma/client
# pnpm
pnpm add @vritti/api-sdk @nestjs/jwt @nestjs/config @prisma/client
For REST APIs and GraphQL gateways that serve HTTP requests:
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 {}
For microservices that process messages from queues:
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 {}
JWT_SECRET=your-access-token-secret-key
# 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=public
# Optional
JWT_REFRESH_SECRET=your-refresh-token-secret-key
PRIMARY_DB_SSL_MODE=prefer # Options: require, prefer, disable
Use @Public() to bypass authentication:
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);
}
}
Use @Onboarding() for registration/verification flows:
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);
}
}
Use @Tenant() to inject tenant metadata:
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);
}
}
Access tenant-specific database connections:
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 });
}
}
The SDK provides base repository classes for common CRUD operations with automatic tenant scoping:
For entities in the primary/platform database (tenants, users, sessions, etc.):
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<User | null> {
return this.model.findUnique({ where: { email } });
}
async findActiveUsers(): Promise<User[]> {
return this.model.findMany({
where: { status: 'ACTIVE' },
orderBy: { createdAt: 'desc' },
});
}
}
For tenant-scoped entities (products, orders, customers, etc.):
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<Product | null> {
return this.model.findUnique({ where: { sku } });
}
async findInStock(): Promise<Product[]> {
return this.model.findMany({
where: { quantity: { gt: 0 } },
});
}
}
Both PrimaryBaseRepository and TenantBaseRepository provide these methods:
// 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' });
// ✅ 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!
forServer())How it works:
x-tenant-id header)TenantContextInterceptor extracts tenant identifierPrimaryDatabaseService queries tenant registry for configurationVrittiAuthGuard validates JWT tokens and tenant statusTenantContextServiceTenant Resolution:
acme.api.vritti.com → acme)x-tenant-id headerforMicroservice())How it works:
MessageTenantContextInterceptor extracts tenant from message payloadTenantContextServiceExpected Message Format:
{
dto: { /* your data */ },
tenant: {
id: 'tenant-uuid',
subdomain: 'acme',
type: 'ENTERPRISE',
databaseHost: 'tenant-db.aws.com',
databaseName: 'acme_db',
// ... other config
}
}
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.
Import the LoggerModule in your application:
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:
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;
}
}
}
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 |
Choose between NestJS default Logger or Winston:
// 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:
# In .env file
LOG_PROVIDER=default # or 'winston'
Important: When using LOG_PROVIDER=default, update your main.ts to avoid circular references:
async function bootstrap() {
const logProvider = process.env.LOG_PROVIDER || 'winston';
const useBuiltInLogger = logProvider === 'default';
const app = await NestFactory.create<NestFastifyApplication>(
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
}
HTTP logging is automatically enabled when enableHttpLogger: true. The interceptor is registered globally:
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]
Correlation IDs are automatically included in all logs when the middleware is registered:
// 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
Override preset defaults for specific needs:
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
},
})
Add custom metadata to enrich your logs (Winston only):
this.logger.logWithMetadata(
'log',
'Payment processed',
{
orderId: order.id,
amount: order.total,
paymentMethod: 'credit_card',
},
'PaymentService'
);
Create context-specific loggers:
@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"
}
}
| 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 |
DatabaseModuleforServer(options): Configure for Gateway/HTTP modeforMicroservice(options): Configure for RabbitMQ/messaging modeAuthConfigModuleforRootAsync(): Register JWT authentication with global guardTenantDatabaseServiceAccess tenant-specific database connections.
class TenantDatabaseService {
async getClient<T = any>(): Promise<T>
clearConnection(tenantId: string): void
}
PrimaryDatabaseServiceAccess the primary/platform database (tenant registry). Use this for cloud-api operations like managing tenants, users, sessions, etc.
class PrimaryDatabaseService {
async getPrimaryDbClient<T = any>(): Promise<T>
async getTenantInfo(identifier: string): Promise<TenantInfo | null>
}
Example:
@Injectable()
export class TenantRepository {
constructor(private readonly database: PrimaryDatabaseService) {}
async findAll() {
const prisma = await this.database.getPrimaryDbClient<PrismaClient>();
return prisma.tenant.findMany();
}
}
TenantContextServiceManage request-scoped tenant context.
class TenantContextService {
getTenant(): TenantInfo
setTenant(tenant: TenantInfo): void
hasTenant(): boolean
clearTenant(): void
}
@Public()Bypass authentication on specific endpoints.
@Onboarding()Accept only onboarding tokens (for registration/verification flows).
@Tenant()Inject tenant metadata into controller methods.
TenantInfointerface 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';
}
DatabaseModuleOptionsinterface 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
}
git clone https://github.com/vritti-ai-platforms/api-sdk.git
cd api-sdk
yarn install
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 onlyyarn test - Run testsyarn test:watch - Run tests in watch modeyarn lint - Lint source filesyarn format - Format code with Prettieryarn clean - Remove build artifactsNote on Type Checking:
build and dev scripts now include automatic type checking via tsc --noEmittype-check script is still available for standalone type checkingThe build process includes the following steps:
tsc --noEmit validates TypeScript types without emitting filestsup bundles the code using the configuration in tsup.config.tsIf type checking fails, the build will not proceed. This ensures published packages are type-safe.
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
Always use ConfigService and validate environment variables at startup:
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<string, unknown>) {
const validatedConfig = plainToClass(EnvironmentVariables, config, {
enableImplicitConversion: true,
});
const errors = validateSync(validatedConfig, {
skipMissingProperties: false,
});
if (errors.length > 0) {
throw new Error(errors.toString());
}
return validatedConfig;
}
Let the SDK manage connection pooling. Don't create custom Prisma instances:
// ✅ 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
}
Always use @Tenant() decorator instead of manually accessing TenantContextService:
// ✅ 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
}
Cause: DatabaseModule not imported or registered incorrectly.
Solution: Ensure DatabaseModule.forServer() or forMicroservice() is imported in your module.
Cause: Missing JWT_SECRET environment variable.
Solution: Add JWT_SECRET to your .env file.
Cause: Request missing subdomain and x-tenant-id header.
Solution: Ensure requests include tenant identifier:
https://acme.api.vritti.comx-tenant-id: acmeCause: Too many concurrent tenants or connections not released.
Solution: Increase maxConnections in DatabaseModule options:
DatabaseModule.forServer({
useFactory: () => ({
// ...
maxConnections: 20, // Increase from default 10
}),
})
If you're migrating from a manual setup:
@Tenant() decoratorBefore:
@Module({
imports: [RequestModule],
providers: [
{ provide: APP_GUARD, useClass: VrittiAuthGuard },
{ provide: APP_INTERCEPTOR, useClass: TenantContextInterceptor },
],
})
After:
@Module({
imports: [
DatabaseModule.forServer({ /* config */ }),
AuthConfigModule.forRootAsync(),
],
})
Contributions are welcome! Please follow these steps:
git checkout -b feature/my-featureyarn test && yarn lintgit commit -am 'Add new feature'git push origin feature/my-featureMIT © Shashank Raju
Shashank Raju
FAQs
NestJS SDK for multi-tenant applications with automatic database routing, JWT authentication, and request-scoped tenant context management.
We found that @vritti/api-sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.