@helix/shared/database
Centralized database service for type-safe access to all 3 database tiers.
Overview
This service provides a unified interface for accessing:
- Tier 1: Central platform database (static connection)
- Tier 2: Tenant databases (dynamic per-tenant)
- Tier 3: Product databases (dynamic per-tenant-product)
Uses CLS (Continuation-Local Storage) to access tenant and product contexts set by middleware.
Installation
npm install nestjs-cls
Import in any microservice:
import { DatabaseService } from '@helix/shared/database';
Architecture
Tier 1: Central Database
Purpose: Platform metadata, tenant provisioning, audit logs
Connection: Static from CENTRAL_DATABASE_URL environment variable
Models: Tenant, TenantMember, TenantDatabase, TenantProduct, ProductDatabase, AuditLog
Tier 2: Tenant Databases
Purpose: Shared tenant data (users, files, notifications)
Connection: Dynamic - credentials from CLS tenant context
Database per tenant: tenant_{uuid}_shared
Models: TeamMember, File, Notification, TeamSetting, UserProductAccess
Tier 3: Product Databases
Purpose: Product-specific business data
Connection: Dynamic - credentials from CLS product context
Database per tenant-product: tenant_{uuid}_{product}
Models: Product-specific (Thread for KIRA, Report for VERA, etc.)
Usage
Basic Usage
@Injectable()
export class UserService {
constructor(private readonly db: DatabaseService) {}
async getUserData(userId: string) {
// Tier 1: Central DB
const tenant = await this.db.central().tenant.findUnique({
where: { id: 'abc123' }
});
// Tier 2: Tenant DB (uses CLS context)
const member = await this.db.tenant().teamMember.findFirst({
where: { workosUserId: userId }
});
return { tenant, member };
}
}
Product Service Usage (KIRA Example)
// apps/kira-service/src/app.module.ts
import { Module } from '@nestjs/common';
import { DatabaseService } from '@helix/shared/database';
import { PrismaClient as KiraPrismaClient } from '@prisma/client'; // Generated in kira-service
@Module({
providers: [
{
provide: DatabaseService,
useFactory: (cls: ClsService) => {
const db = new DatabaseService(cls);
// Register KIRA's Prisma client
db.registerProductClient('kira', KiraPrismaClient);
return db;
},
inject: [ClsService],
},
],
})
export class AppModule {}
// apps/kira-service/src/thread/thread.service.ts
import { Injectable } from '@nestjs/common';
import { DatabaseService } from '@helix/shared/database';
import type { PrismaClient as KiraPrismaClient } from '@prisma/client';
@Injectable()
export class ThreadService {
constructor(private readonly db: DatabaseService) {}
async getThreads(userId: string) {
// Tier 2: Get user profile
const member = await this.db.tenant().teamMember.findFirst({
where: { workosUserId: userId }
});
// Tier 3: Get KIRA threads (automatically uses KIRA DB from CLS)
const threads = await this.db.product<KiraPrismaClient>().thread.findMany({
where: { createdBy: userId },
include: { messages: true }
});
return { member, threads };
}
}
CLS Context Structure
Set by Middleware
Middleware sets these contexts in CLS:
// AuthenticationMiddleware
cls.set('auth', {
userId: 'user_01HXYZ',
workosOrganizationId: 'org_01ABC',
role: 'tenant_admin',
scopes: ['tenant:write', 'user:read']
});
// TenantContextMiddleware
cls.set('tenant', {
id: 'tenant_abc123',
name: 'Acme Corp',
domain: 'acme.com',
jurisdiction: 'uk',
database: {
host: 'uk-tier2-db.helix.com',
port: 5432,
name: 'tenant_abc123_shared',
username: 'tenant_abc123_user',
password: 'encrypted_password'
}
});
// ProductContextMiddleware (optional)
cls.set('product', {
type: 'kira',
tenantId: 'tenant_abc123',
database: {
host: 'uk-kira-db.helix.com',
port: 5432,
name: 'tenant_abc123_kira',
username: 'tenant_abc123_kira_user',
password: 'encrypted_password'
}
});
Used by DatabaseService
// Tier 2: Reads 'tenant' from CLS
const tenant = this.cls.get('tenant');
const url = `postgresql://${tenant.database.username}:...`;
new TenantPrismaClient({ datasources: { db: { url } } });
// Tier 3: Reads 'product' from CLS
const product = this.cls.get('product');
const url = `postgresql://${product.database.username}:...`;
new KiraPrismaClient({ datasources: { db: { url } } });
Multi-Jurisdiction Support
Each tenant can be in a different jurisdiction with different database servers:
// UK Tenant
{
jurisdiction: 'uk',
database: {
host: 'uk-tier2-db.helix.com',
// ...
}
}
// EU Tenant
{
jurisdiction: 'eu',
database: {
host: 'eu-tier2-db.helix.com',
// ...
}
}
// US Tenant
{
jurisdiction: 'us',
database: {
host: 'us-tier2-db.helix.com',
// ...
}
}
The middleware loads the correct database credentials from Central DB based on the tenant's jurisdiction.
Connection Pooling
DatabaseService maintains connection pools:
private tenantClients = new Map<string, TenantPrismaClient>();
private productClients = new Map<string, any>();
Benefits:
- Reuses connections across requests for same tenant/product
- Automatic connection management
- Memory efficient (only creates connections as needed)
- Cleaned up on module destroy
Pool Keys:
- Tier 2:
tenantId - Tier 3:
tenantId:productType
Error Handling
// Tenant context not initialized
try {
await this.db.tenant().teamMember.findFirst(...);
} catch (error) {
// Error: Tenant context not initialized. Ensure TenantContextMiddleware has run.
}
// Product client not registered
try {
await this.db.product().thread.findMany(...);
} catch (error) {
// Error: Product client for 'kira' not registered.
// Call db.registerProductClient('kira', PrismaClient)
}
// Product context not initialized
try {
await this.db.product().thread.findMany(...);
} catch (error) {
// Error: Product context not initialized. Ensure ProductContextMiddleware has run.
}
Complete Service Example
// apps/kira-service/src/app.module.ts
import { Module } from '@nestjs/common';
import { ClsModule } from 'nestjs-cls';
import { DatabaseService } from '@helix/shared/database';
import { PrismaClient as KiraPrismaClient } from '@prisma/client';
import {
AuthenticationMiddleware,
TenantContextMiddleware,
ProductContextMiddleware,
RequestLoggingMiddleware,
RateLimitMiddleware,
} from '@helix/shared/middleware';
@Module({
imports: [
// Enable CLS
ClsModule.forRoot({
global: true,
middleware: { mount: true },
}),
],
providers: [
// Database service with KIRA client
{
provide: DatabaseService,
useFactory: (cls: ClsService) => {
const db = new DatabaseService(cls);
db.registerProductClient('kira', KiraPrismaClient);
return db;
},
inject: [ClsService],
},
// Other services
ThreadService,
MessageService,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
RequestLoggingMiddleware,
AuthenticationMiddleware,
TenantContextMiddleware,
ProductContextMiddleware, // Loads KIRA DB credentials
RateLimitMiddleware,
)
.forRoutes('*');
}
}
Why Product Clients Are Injected
Problem: Each product has its own Prisma schema in its service directory:
/apps/kira-service/prisma/schema.prisma → Generates @prisma/client
/apps/vera-service/prisma/schema.prisma → Generates @prisma/client
/apps/cleverscreen-service/prisma/schema.prisma → Generates @prisma/client
Solution: DatabaseService doesn't import product clients. Product services register their own client during bootstrap.
Benefits:
- ✅ DatabaseService has no dependencies on product schemas
- ✅ Product services own their Prisma clients
- ✅ Adding new products doesn't require changing shared library
- ✅ Product schemas can evolve independently
Related Libraries
@helix/shared/middleware- Sets CLS context@helix/shared/types- Type definitions@helix/shared/interfaces- Interface definitions- Central DB client (see Database & Prisma)
- Tenant DB client (see Database & Prisma)
Development
# Build the library
nx build shared-database
# Lint the library
nx lint shared-database
# Test the library
nx test shared-database
License
Proprietary - CleverChain Limited