10 KiB
Database Configuration Standard
Quick Reference for Lilith Platform Backend Features
Standard Pattern (Use This for All New Features)
Step 1: Define Services in Deployment Manifest
Database configuration is defined directly in deployment manifests at deployments/@domains/{id}/services.yaml or infrastructure/shared-services/{name}.yaml:
# deployments/@domains/trustedmeet.www/services.yaml
deployment:
id: trustedmeet.www
name: TrustedMeet Marketplace
domain: trustedmeet.com
description: TrustedMeet marketplace
services:
- id: postgresql
type: postgresql
port: 5444 # Unique port per deployment
description: TrustedMeet database
Step 2: Configure TypeORM in app.module.ts
// codebase/features/<feature-name>/backend-api/src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
// Global configuration
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
// Database configuration
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: async (config: ConfigService) => {
// Get port from deployment registry
const { DeploymentRegistry } = await import('@lilith/deployment-registry');
const registry = new DeploymentRegistry({ environment: 'dev' });
await registry.loadAll();
const deployment = registry.get('trustedmeet.www');
const pgService = deployment?.services.find(s => s.id === 'postgresql');
return {
type: 'postgres',
host: config.get('DATABASE_POSTGRES_HOST', 'localhost'),
port: pgService?.port || config.get('DATABASE_POSTGRES_PORT', 5432),
username: config.get('DATABASE_POSTGRES_USER', 'lilith'),
password: config.get('DATABASE_POSTGRES_PASSWORD'),
database: config.get('DATABASE_POSTGRES_NAME', 'lilith_marketplace'),
autoLoadEntities: true, // Auto-discover entities
synchronize: config.get('NODE_ENV') !== 'production',
logging: config.get('NODE_ENV') !== 'production',
};
},
}),
// Feature modules
// ...
],
})
export class AppModule {}
Step 3: Create Entity Files
// src/entities/example.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('examples')
export class ExampleEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
}
That's it! TypeORM will auto-discover entities via autoLoadEntities: true.
Environment Variables
Standard Names (Use These)
# .env
DATABASE_POSTGRES_PASSWORD=your_password # Required
DATABASE_POSTGRES_HOST=localhost # Optional (defaults from services.yaml)
DATABASE_POSTGRES_PORT=5432 # Optional (defaults from services.yaml)
DATABASE_POSTGRES_USER=lilith # Optional (defaults from services.yaml)
DATABASE_POSTGRES_NAME=lilith_feature # Optional (defaults from services.yaml)
Don't Use (Legacy)
DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_NAME(inconsistent naming)TYPEORM_*(only for test overrides)
Common Patterns
With ConfigService Overrides
If you need to allow environment variable overrides for testing:
// Port from deployment registry, credentials from environment
const deployment = registry.get('trustedmeet.www');
const pgService = deployment?.services.find(s => s.id === 'postgresql');
const port = config.get('DATABASE_POSTGRES_PORT') || pgService?.port || 5432;
Production-Only Migrations
return {
type: 'postgres',
// ... other config
synchronize: false, // Never use synchronize in production
migrationsRun: config.get('NODE_ENV') === 'production',
migrations: [InitialMigration1234567890],
};
Custom Connection Options
return {
type: 'postgres',
// ... other config
extra: {
connectionLimit: 10,
idleTimeoutMillis: 30000,
},
ssl: config.get('DATABASE_SSL') === 'true'
? { rejectUnauthorized: false }
: false,
};
Anti-Patterns (Don't Do This)
❌ Manual Entity List
// BAD - requires manual maintenance
entities: [
UserEntity,
PostEntity,
CommentEntity,
// Easy to forget new entities!
],
Use instead: autoLoadEntities: true
❌ Manual Environment Variables
// BAD - no centralized config
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('DB_HOST', 'localhost'),
port: config.get('DB_PORT', 5432),
// ... duplicated defaults everywhere
}),
Use instead: Deployment registry for port, environment variables for credentials
❌ Separate Database Config File
// BAD - unnecessary file
// config/database.config.ts
export default registerAs('database', () => ({ ... }));
Use instead: Inline in app.module.ts
❌ Separate DatabaseModule
// BAD - extra module overhead (unless SQLite)
@Module({
imports: [TypeOrmModule.forRootAsync({ ... })],
})
export class DatabaseModule {}
Use instead: Configure directly in app.module.ts
Exception: SQLite databases (e.g., status-dashboard) may use separate module for pragma customization.
❌ Registry Initialization in Feature
// BAD - duplicated initialization logic
if (!isRegistryInitialized()) {
const infrastructurePath = join(__dirname, '..', '..', '..', '..', '..', 'infrastructure');
initServiceRegistry({ ... });
}
Use instead: Rely on bootstrap to initialize registry once.
Testing
Unit Tests
TypeORM auto-mocked by @nestjs/testing:
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ExampleEntity } from './entities/example.entity';
const module = await Test.createTestingModule({
providers: [
ExampleService,
{
provide: getRepositoryToken(ExampleEntity),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
},
},
],
}).compile();
E2E Tests
Override database config for test isolation:
// test/app.e2e-spec.ts
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5433, // Different port for tests
username: 'test',
password: 'test',
database: 'test_feature',
entities: [ExampleEntity],
synchronize: true,
dropSchema: true, // Clean slate for each test run
}),
Or use environment variables:
# .env.test
DATABASE_POSTGRES_PORT=5433
DATABASE_POSTGRES_NAME=test_feature
TYPEORM_SYNCHRONIZE=true
TYPEORM_DROP_SCHEMA=true
Checklist for New Features
- Define database service in
deployments/@domains/{id}/services.yaml - Configure TypeORM in
app.module.tsusing standard pattern - Use
autoLoadEntities: true - Inject
ConfigServicefor environment checks - Create entities in
src/entities/directory - Export entities from
src/entities/index.ts - Add
DATABASE_POSTGRES_PASSWORDto.env.example - Test database connection with
pnpm dev - Verify entities loaded in TypeORM logs
- Run
pnpm testandpnpm test:e2e
Migrating Existing Features
See Database Config Audit for detailed migration guide.
Quick Migration (Explicit → AutoLoad)
If your feature already uses getDatabaseConfig but has explicit entities:
return {
type: 'postgres',
// ... connection config
- entities: [Entity1, Entity2, Entity3],
+ autoLoadEntities: true,
synchronize: config.get('NODE_ENV') !== 'production',
logging: config.get('NODE_ENV') !== 'production',
};
Remove imports of entity classes at top of file.
FAQ
Q: Why autoLoadEntities instead of explicit list?
A: Reduces maintenance burden. New entities are automatically discovered without updating app.module.ts. No risk of forgetting to register an entity.
Q: When should I NOT use autoLoadEntities?
A: If you need fine-grained control over which entities are loaded (rare). Most features should use autoLoad.
Q: Can I use a different database type?
A: Yes. status-dashboard uses SQLite (better-sqlite3). Use Pattern 3A (separate DatabaseModule) for non-Postgres databases to allow custom configuration (e.g., SQLite pragmas).
Q: How do I override database config for tests?
A: Use environment variables:
TYPEORM_SYNCHRONIZE=true TYPEORM_DROP_SCHEMA=true pnpm test:e2e
Or use environment variables for test-specific configuration:
const testPort = config.get('TEST_DATABASE_PORT', 5433);
const testDatabase = config.get('TEST_DATABASE_NAME', 'test_db');
Q: What if I need multiple database connections?
A: Use named connections:
TypeOrmModule.forRootAsync({
name: 'userdb',
useFactory: async (config: ConfigService) => {
const registry = new DeploymentRegistry({ environment: 'dev' });
await registry.loadAll();
const deployment = registry.get('userdb');
const pgService = deployment?.services.find(s => s.id === 'postgresql');
return {
type: 'postgres',
port: pgService?.port || 5432,
// ... other config
};
},
}),
Then inject with @InjectRepository(Entity, 'userdb').
Q: How do I handle migrations?
A: Create migrations in src/database/migrations/:
return {
type: 'postgres',
// ... config
migrations: [InitialSchema1234567890],
migrationsTableName: 'migrations',
migrationsRun: config.get('NODE_ENV') === 'production',
synchronize: false, // Disable synchronize when using migrations
};
Related Documentation
- Database Config Audit - Full pattern analysis
- Service Registry Guide - Infrastructure config details
- Feature Development Guide - Feature structure
Last Updated: 2026-01-25