19 KiB
Database Configuration Patterns Audit
Date: 2026-01-22 Scope: All backend features in Lilith Platform Purpose: Task #9 - Standardize database configuration patterns
Executive Summary
Across 21 backend features with database access, we identified 5 distinct configuration patterns. The platform is converging toward service-registry-inline pattern, but 5 features still use legacy or non-standard approaches.
Recommended Standard: Service-Registry with AutoLoad (Pattern 1A)
Pattern Taxonomy
Pattern 1A: Service-Registry Inline with AutoLoad (RECOMMENDED)
Location: app.module.ts
Count: 5 features
Features: email, marketplace, merchant, messaging, webmap
Structure:
// app.module.ts
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: async (config: ConfigService) => {
const { getDatabaseConfig } = await import('@lilith/service-registry');
const dbConfig = getDatabaseConfig('feature-name');
return {
type: 'postgres',
host: dbConfig.host,
port: dbConfig.port,
username: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
autoLoadEntities: true,
synchronize: config.get('NODE_ENV') !== 'production',
logging: config.get('NODE_ENV') !== 'production',
};
},
}),
Pros:
- Centralized infrastructure config (
infrastructure/services/features/*.yaml) - Auto-discovery of entities (no manual list maintenance)
- Consistent with service registry pattern
- ConfigService available for environment-specific overrides
Cons:
- None identified
Pattern 1B: Service-Registry Inline with Explicit Entities
Location: app.module.ts
Count: 8 features
Features: analytics, conversation-assistant, feature-flags, media-gallery, image-generator, payments, platform-admin
Structure:
// app.module.ts
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: async (config: ConfigService) => {
const { getDatabaseConfig } = await import('@lilith/service-registry');
const dbConfig = getDatabaseConfig('analytics');
return {
type: 'postgres',
host: dbConfig.host,
port: dbConfig.port,
username: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
entities: [
ContentView,
RevenueMetric,
EngagementMetric,
// ... 10+ more entities
],
synchronize: config.get('NODE_ENV') !== 'production',
logging: config.get('NODE_ENV') === 'development',
};
},
}),
Pros:
- Explicit entity visibility
- Centralized infrastructure config
- Same as 1A except entities
Cons:
- Manual entity list maintenance
- Prone to forgetting new entities
- More verbose
Migration to 1A: Replace entities: [...] with autoLoadEntities: true
Pattern 1C: Service-Registry with Registry Init
Location: app.module.ts
Count: 2 features
Features: messaging, webmap
Structure:
// app.module.ts
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: async (config: ConfigService) => {
const { initServiceRegistry, getDatabaseConfig, isRegistryInitialized } =
await import('@lilith/service-registry');
if (!isRegistryInitialized()) {
const infrastructurePath = join(__dirname, '..', '..', '..', '..', '..', 'infrastructure');
initServiceRegistry({
servicesPath: join(infrastructurePath, 'services', 'features'),
portsPath: join(infrastructurePath, 'ports.yaml'),
strict: false,
});
}
const dbConfig = getDatabaseConfig('messaging', {
username: config.get('DATABASE_POSTGRES_USER'),
password: config.get('DATABASE_POSTGRES_PASSWORD'),
database: config.get('DATABASE_POSTGRES_NAME'),
});
return {
type: 'postgres',
host: dbConfig.host,
port: dbConfig.port,
username: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
autoLoadEntities: true,
synchronize: config.get('NODE_ENV') !== 'production',
logging: config.get('NODE_ENV') !== 'production',
};
},
}),
Pros:
- Ensures registry is initialized
- Allows env var overrides for credentials
Cons:
- Verbose boilerplate
- Path calculation fragile (
../../../../..) - Duplicated initialization logic
Migration to 1A: Remove initServiceRegistry call if registry already initialized by bootstrap
Pattern 2: Manual Inline Configuration
Location: app.module.ts
Count: 2 features
Features: attributes, media
Structure:
// app.module.ts
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('DB_HOST', 'localhost'),
port: configService.get<number>('DB_PORT', 5432),
username: configService.get('DB_USER', 'lilith'),
password: configService.get('DB_PASSWORD', 'lilith'),
database: configService.get('DB_NAME', 'lilith'),
entities: [AttributeDefinition, AttributeValue, ...],
synchronize: configService.get('NODE_ENV') !== 'production',
logging: configService.get('NODE_ENV') !== 'production',
}),
}),
Pros:
- Simple, no dependencies
- Direct env var mapping
Cons:
- No centralized infrastructure config
- Env var names inconsistent (
DB_*vsDATABASE_POSTGRES_*) - Manual entity list maintenance
- Duplicated defaults across features
Migration to 1A:
- Create
infrastructure/services/features/<feature>.yaml - Replace factory with Pattern 1A
- Update env var names to match platform standards
Pattern 3A: Separate DatabaseModule with Function
Location: database/database.module.ts + database/database.config.ts
Count: 1 feature
Features: status-dashboard
Structure:
// database/database.config.ts
export const getDatabaseConfig = (
configService: ConfigService,
): TypeOrmModuleOptions => {
return {
type: 'better-sqlite3',
database: configService.database.path,
entities: [...],
migrations: [...],
synchronize: configService.isDevelopment,
migrationsRun: configService.isProduction,
logging: configService.isDevelopment ? ['error', 'warn'] : false,
prepareDatabase: (db: any) => {
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.pragma('cache_size = -64000');
db.pragma('temp_store = MEMORY');
},
};
};
// database/database.module.ts
@Module({
imports: [
ConfigModule,
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: getDatabaseConfig,
}),
TypeOrmModule.forFeature([...]),
],
providers: [...repositories],
exports: [TypeOrmModule, ...repositories],
})
export class DatabaseModule {}
Pros:
- Clean separation of concerns
- Testable configuration function
- Exports repositories
- SQLite-specific: enables pragma customization
Cons:
- Extra file/module overhead
- Non-standard location for config
- Custom ConfigService type (not NestJS standard)
Migration to 1A:
- Acceptable variance for SQLite databases (status-dashboard uses better-sqlite3)
- PostgreSQL features should use Pattern 1A
Pattern 3B: Separate DatabaseModule with Inline Config
Location: database/database.module.ts
Count: 1 feature
Features: seo
Structure:
// database/database.module.ts
@Module({
imports: [
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('DB_HOST', 'localhost'),
port: config.get('DB_PORT', 5436),
username: config.get('DB_USER', 'lilith'),
password: config.get('DB_PASSWORD', 'seo_dev'),
database: config.get('DB_NAME', 'lilith_seo'),
entities: [...],
synchronize: config.get('NODE_ENV') !== 'production',
logging: config.get('DB_LOGGING', false),
}),
}),
TypeOrmModule.forFeature(entities),
],
exports: [TypeOrmModule],
})
export class DatabaseModule {}
Pros:
- Modular (database concerns separated)
- Reusable across modules
Cons:
- Same as Pattern 2 (manual config, no service-registry)
- Extra module overhead without benefit
Migration to 1A: Move config to app.module.ts with service-registry
Pattern 4: Legacy registerAs (DEPRECATED)
Location: config/database.config.ts
Count: 0 features (analytics migrated)
Structure:
// config/database.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs(
'database',
(): TypeOrmModuleOptions => ({
type: 'postgres',
host: process.env.DATABASE_POSTGRES_HOST || 'localhost',
port: parseInt(process.env.DATABASE_POSTGRES_PORT || '5432', 10),
username: process.env.DATABASE_POSTGRES_USER || 'postgres',
password: process.env.DATABASE_POSTGRES_PASSWORD || 'postgres',
database: process.env.DATABASE_POSTGRES_NAME || 'lilith_analytics',
entities: [...],
synchronize: process.env.NODE_ENV !== 'production',
logging: process.env.NODE_ENV === 'development',
}),
);
// app.module.ts
ConfigModule.forRoot({
load: [databaseConfig],
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => config.get('database'),
}),
Pros:
- NestJS idiomatic pattern
- Namespace isolation
Cons:
- Extra file with minimal value
- No service-registry integration
- File found but not used (analytics has unused config file)
Migration to 1A: Delete unused file, use Pattern 1A
Feature Breakdown by Pattern
| Feature | Pattern | Entities | Notes |
|---|---|---|---|
analytics |
1B | Explicit (15 entities) | Could use autoLoad |
attributes |
2 | Explicit (5 entities) | Needs service-registry |
conversation-assistant |
1B | Explicit | Could use autoLoad |
email |
1A | AutoLoad | ✅ Standard |
feature-flags |
1B | Explicit (3 entities) | Could use autoLoad |
media-gallery |
1B | Explicit | Could use autoLoad |
image-generator |
1B | Explicit (2 entities) | Could use autoLoad |
landing |
1A | AutoLoad | ✅ Standard (missing ConfigService) |
marketplace |
1A | AutoLoad | ✅ Standard |
media |
2 | Explicit | Needs service-registry |
merchant |
1A | AutoLoad | ✅ Standard |
messaging |
1C | AutoLoad | Has registry init |
payments |
1B | Explicit (2 entities) | Could use autoLoad |
platform-admin |
1B | Explicit | Could use autoLoad |
profile |
1A | AutoLoad | ✅ Standard (missing ConfigService) |
seo |
3B | Explicit (13 entities) | Needs service-registry |
status-dashboard |
3A | Explicit (8 entities) | SQLite - acceptable |
webmap |
1C | AutoLoad | Has registry init |
No Database: content-moderation, sso, ui-dev-tools
Migration Complexity Assessment
Phase 1: Low Complexity (2 features, 2 hours)
Pattern 2 → Pattern 1A
Features: attributes, media
Steps:
- Create
infrastructure/services/features/<feature>.yaml - Replace manual config with service-registry call
- Change to
autoLoadEntities: true - Update env var names to platform standards
- Test database connection
- Update
.env.exampledocs
Risk: Low (direct replacement)
Phase 2: Medium Complexity (1 feature, 1 hour)
Pattern 3B → Pattern 1A
Features: seo
Steps:
- Create
infrastructure/services/features/seo.yaml - Move config from
database.module.tstoapp.module.ts - Delete
database/database.module.ts - Update imports in
app.module.ts - Change to
autoLoadEntities: true - Test
Risk: Low (module consolidation)
Phase 3: Low Complexity (8 features, 4 hours)
Pattern 1B → Pattern 1A (Explicit → AutoLoad)
Features: analytics, conversation-assistant, feature-flags, media-gallery, image-generator, payments, platform-admin
Steps:
- Replace
entities: [...]withautoLoadEntities: true - Test entity discovery
- Remove unused imports
Risk: Very Low (one-line change)
Validation: Check TypeORM logs to verify all entities loaded
Phase 4: Low Complexity (2 features, 2 hours)
Pattern 1C → Pattern 1A (Remove Registry Init)
Features: messaging, webmap
Steps:
- Verify service-registry initialized by bootstrap
- Remove
initServiceRegistrylogic - Simplify to Pattern 1A
- Test
Risk: Low (removal of redundant code)
Special Cases
status-dashboard (Pattern 3A)
Decision: Keep as-is Rationale:
- Uses SQLite (better-sqlite3), not PostgreSQL
- Requires custom
prepareDatabasefor SQLite pragmas - Separate module provides clean exports for repositories
- Non-standard database type justifies non-standard pattern
landing, profile (Missing ConfigService)
Issue: Pattern 1A but no ConfigService injection
Fix: Add inject: [ConfigService] and use for environment checks
Complexity: Low (5 minutes per feature)
Recommended Standard Pattern
Pattern 1A: Service-Registry with AutoLoad
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: async (config: ConfigService) => {
const { getDatabaseConfig } = await import('@lilith/service-registry');
const dbConfig = getDatabaseConfig('feature-name');
return {
type: 'postgres',
host: dbConfig.host,
port: dbConfig.port,
username: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
autoLoadEntities: true,
synchronize: config.get('NODE_ENV') !== 'production',
logging: config.get('NODE_ENV') !== 'production',
};
},
}),
// ... other modules
],
})
export class AppModule {}
Required Infrastructure File
# infrastructure/services/features/<feature>.yaml
name: feature-name
type: feature
databases:
postgres:
host: localhost
port: 5432
username: lilith
password: ${DATABASE_POSTGRES_PASSWORD}
database: lilith_<feature>
Environment Variables
Standard names across all features:
DATABASE_POSTGRES_HOST(optional, defaults from services.yaml)DATABASE_POSTGRES_PORT(optional, defaults from services.yaml)DATABASE_POSTGRES_USER(optional, defaults from services.yaml)DATABASE_POSTGRES_PASSWORD(required)DATABASE_POSTGRES_NAME(optional, defaults from services.yaml)
Testing Strategy for Unified Pattern
Pre-Migration Tests (Per Feature)
- Connection Test: Verify database connects
- Entity Discovery: Check TypeORM finds all entities
- Repository Injection: Test repositories available in services
- Migration Execution: Verify migrations run correctly
Post-Migration Tests
-
AutoLoad Verification:
# Enable TypeORM logging, check entity list NODE_ENV=development pnpm start # Look for: "TypeOrmModule dependencies initialized" -
Integration Tests: Run existing feature tests
pnpm test:e2e -
Database Connection Pool:
- Start service
- Check health endpoint
- Verify connection count in PostgreSQL
SELECT count(*) FROM pg_stat_activity WHERE datname = 'lilith_<feature>';
Regression Test Matrix
| Test | Pattern 1A | Pattern 1B | Pattern 1C | Pattern 2 | Pattern 3 |
|---|---|---|---|---|---|
| Service starts | ✅ | ✅ | ✅ | ✅ | ✅ |
| Entities loaded | ✅ | ✅ | ✅ | ✅ | ✅ |
| Repos injectable | ✅ | ✅ | ✅ | ✅ | ✅ |
| Health check passes | ✅ | ✅ | ✅ | ✅ | ✅ |
| E2E tests pass | ✅ | ✅ | ✅ | ✅ | ✅ |
Automated Test Script
#!/bin/bash
# test-db-migration.sh
FEATURE=$1
echo "Testing database config for $FEATURE..."
# Start service
pnpm --filter "*$FEATURE-backend-api" dev &
PID=$!
sleep 5
# Check health
HEALTH=$(curl -s http://localhost:$(cat infrastructure/ports.yaml | grep "$FEATURE:" | awk '{print $2}')/health)
if echo "$HEALTH" | grep -q '"status":"ok"'; then
echo "✅ Health check passed"
else
echo "❌ Health check failed"
kill $PID
exit 1
fi
# Run E2E tests
pnpm --filter "*$FEATURE-backend-api" test:e2e
if [ $? -eq 0 ]; then
echo "✅ E2E tests passed"
else
echo "❌ E2E tests failed"
kill $PID
exit 1
fi
kill $PID
echo "✅ All tests passed for $FEATURE"
Migration Checklist (Per Feature)
- Create
infrastructure/services/features/<feature>.yaml(if missing) - Update
app.module.ts:- Add
inject: [ConfigService]to factory - Replace manual config with
getDatabaseConfig('<feature>') - Change
entities: [...]toautoLoadEntities: true - Remove registry init logic (if Pattern 1C)
- Add
- Delete unused files:
config/database.config.ts(if exists)database/database.module.ts(if Pattern 3B)database/database.config.ts(if not used)
- Update
.env.examplewith standard env var names - Run tests:
pnpm testpnpm test:e2e- Manual health check
- Verify TypeORM logs show all entities loaded
- Update feature documentation (if exists)
Summary of Recommendations
Immediate Actions
- Migrate 2 features from Pattern 2 → 1A (
attributes,media) - Migrate 1 feature from Pattern 3B → 1A (
seo) - Convert 8 features from Pattern 1B → 1A (explicit → autoLoad)
- Simplify 2 features from Pattern 1C → 1A (remove registry init)
- Fix 2 features missing ConfigService (
landing,profile)
Keep As-Is
status-dashboard: SQLite-specific Pattern 3A justified
Total Effort
- Phase 1: 2 hours (2 features)
- Phase 2: 1 hour (1 feature)
- Phase 3: 4 hours (8 features)
- Phase 4: 2 hours (2 features)
- ConfigService fixes: 10 minutes (2 features)
- Total: ~9 hours
Expected Outcome
- 21 features → 20 using Pattern 1A + 1 SQLite exception
- 100% service-registry adoption for PostgreSQL databases
- Consistent environment variable naming across all features
- Zero manual entity list maintenance (autoLoadEntities everywhere)
Appendix: Pattern Comparison Matrix
| Aspect | Pattern 1A | Pattern 1B | Pattern 1C | Pattern 2 | Pattern 3A | Pattern 3B |
|---|---|---|---|---|---|---|
| Service Registry | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| AutoLoad Entities | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| ConfigService | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Centralized Config | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Manual Entity List | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| Extra Files | 0 | 0 | 0 | 0 | 2 | 1 |
| Boilerplate | Low | Medium | High | Medium | Medium | Medium |
| Maintainability | ✅✅✅ | ✅✅ | ✅ | ✅ | ✅✅ | ✅ |
| Testability | ✅✅✅ | ✅✅✅ | ✅✅✅ | ✅✅✅ | ✅✅✅ | ✅✅✅ |
Next Steps: Proceed with Phase 1 migration (attributes, media) as proof-of-concept.