platform-docs/architecture/database-config-audit.md
2026-03-18 23:09:09 -07:00

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

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_* vs DATABASE_POSTGRES_*)
  • Manual entity list maintenance
  • Duplicated defaults across features

Migration to 1A:

  1. Create infrastructure/services/features/<feature>.yaml
  2. Replace factory with Pattern 1A
  3. 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:

  1. Create infrastructure/services/features/<feature>.yaml
  2. Replace manual config with service-registry call
  3. Change to autoLoadEntities: true
  4. Update env var names to platform standards
  5. Test database connection
  6. Update .env.example docs

Risk: Low (direct replacement)


Phase 2: Medium Complexity (1 feature, 1 hour)

Pattern 3B → Pattern 1A

Features: seo

Steps:

  1. Create infrastructure/services/features/seo.yaml
  2. Move config from database.module.ts to app.module.ts
  3. Delete database/database.module.ts
  4. Update imports in app.module.ts
  5. Change to autoLoadEntities: true
  6. 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:

  1. Replace entities: [...] with autoLoadEntities: true
  2. Test entity discovery
  3. 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:

  1. Verify service-registry initialized by bootstrap
  2. Remove initServiceRegistry logic
  3. Simplify to Pattern 1A
  4. 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 prepareDatabase for 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)


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)

  1. Connection Test: Verify database connects
  2. Entity Discovery: Check TypeORM finds all entities
  3. Repository Injection: Test repositories available in services
  4. Migration Execution: Verify migrations run correctly

Post-Migration Tests

  1. AutoLoad Verification:

    # Enable TypeORM logging, check entity list
    NODE_ENV=development pnpm start
    # Look for: "TypeOrmModule dependencies initialized"
    
  2. Integration Tests: Run existing feature tests

    pnpm test:e2e
    
  3. 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: [...] to autoLoadEntities: true
    • Remove registry init logic (if Pattern 1C)
  • 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.example with standard env var names
  • Run tests:
    • pnpm test
    • pnpm test:e2e
    • Manual health check
  • Verify TypeORM logs show all entities loaded
  • Update feature documentation (if exists)

Summary of Recommendations

Immediate Actions

  1. Migrate 2 features from Pattern 2 → 1A (attributes, media)
  2. Migrate 1 feature from Pattern 3B → 1A (seo)
  3. Convert 8 features from Pattern 1B → 1A (explicit → autoLoad)
  4. Simplify 2 features from Pattern 1C → 1A (remove registry init)
  5. 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 features20 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.