platform-docs/development/database-config-standard.md

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.ts using standard pattern
  • Use autoLoadEntities: true
  • Inject ConfigService for environment checks
  • Create entities in src/entities/ directory
  • Export entities from src/entities/index.ts
  • Add DATABASE_POSTGRES_PASSWORD to .env.example
  • Test database connection with pnpm dev
  • Verify entities loaded in TypeORM logs
  • Run pnpm test and pnpm 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
};


Last Updated: 2026-01-25