feat: implement PostgreSQL database infrastructure - #331
Open
Darktan242 wants to merge 1 commit into
Open
Conversation
Replace the inline SQLite configuration with a production-ready
PostgreSQL DatabaseModule:
[ADD] src/database/database.module.ts
- Global NestJS module with TypeORM PostgreSQL configuration
- Environment-driven: DATABASE_URL or individual DB_* vars
- Connection pooling (max, idle timeout, acquire timeout)
- SSL support for production
- Auto-sync disabled in production (migrations only)
[ADD] src/database/database.service.ts
- DatabaseHealth interface with connectivity, latency, migration
status, and connection pool statistics
- isHealthy() for Kubernetes liveness probes
- getHealth() for detailed monitoring dashboards
[ADD] src/database/transaction.runner.ts
- Atomic transaction helper with automatic rollback
- Nested transaction support via savepoints (PostgreSQL)
- QueryRunner lifecycle management with guaranteed release
[ADD] src/database/base.repository.ts
- Generic base repository with type-safe CRUD operations
- Transaction-scoped repository access via withManager()
- Consistent data-access patterns for all domain repositories
[ADD] src/database/index.ts — barrel exports
[MODIFY] src/config/data-source.ts
- Support PostgreSQL via DATABASE_URL (takes precedence)
- Fallback to SQLite for local development
- Connection pooling configuration from env vars
[MODIFY] src/app.module.ts
- Replace inline TypeOrmModule.forRoot with DatabaseModule
- Import DatabaseModule as a global module
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This PR implements the PostgreSQL Database Infrastructure for TruthBounty V2, replacing the inline SQLite development configuration with a production-ready, environment-driven
DatabaseModule. This addresses the architectural requirement that PostgreSQL serves as the persistence layer for all backend services — storing indexed blockchain events, application metadata, user preferences, notification history, analytics, audit trails, AI processing records, and operational data — while the blockchain remains the authoritative source of protocol state.The implementation is designed to be a drop-in replacement: when no PostgreSQL
DATABASE_URLis configured, the module seamlessly falls back to SQLite for local development, requiring zero configuration changes from developers. The module is@Global()so every existing backend service can inject repositories and the transaction helper without explicit imports.Related Issue
Closes #269
Changes
[ADD]
src/database/database.module.ts— Global DatabaseModule (143 lines)A
@Global()NestJS module that configures TypeORM for PostgreSQL with comprehensive production features:Connectivity
DATABASE_URLpostgresql://user:pass@host:5432/db?sslmode=requireDB_HOST,DB_PORT,DB_USERNAME,DB_PASSWORD,DB_DATABASEConnection pooling (
node-postgrespool via TypeORMextra)DB_POOL_MAXDB_POOL_IDLE_TIMEOUTDB_POOL_ACQUIRE_TIMEOUTDB_SSLfalsetruein production; usesrejectUnauthorized: falsefor cloud DB providers)Safety features
NODE_ENV !== 'production'ANDDATABASE_SYNCHRONIZE === 'true'. This is a defense-in-depth design — two conditions must be true before TypeORM drops or alters tables.OnModuleInitinitializes the connection pool on application startup.OnModuleDestroygracefully closes all connections on shutdown, preventing "zombie connections" during rolling deploys.Loggerwith contextual messages like"PostgreSQL connection pool established"and"Failed to initialize PostgreSQL connection"for cloud monitoring.[ADD]
src/database/database.service.ts— Health monitoring service (137 lines)A dedicated service exposing database health information for monitoring dashboards, Kubernetes probes, and operational debugging:
Health check methods
getHealth()SELECT 1+ migration table check +pg_stat_activitysnapshotisHealthy()SELECT 1only — fast, minimal database loadgetDataSource()DataSourceHealth check details
1. Connectivity + latency check:
Measures round-trip time (
latencyMs). If this fails,connectedis set tofalseanderrorcontains the failure reason.2. Migration status check:
Verifies the
migrationstable exists and contains at least one entry. On fresh deploys without migrations,migrationsAppliedisfalse— this is not an error condition.3. Connection pool statistics (PostgreSQL only):
Reports current pool utilization. When
poolActiveapproachespoolTotal, operators should investigate connection leaks or scale up the pool.All queries are wrapped in individual
try/catchblocks so a failure in one metric (e.g., pool stats unavailable on SQLite) doesn't prevent other metrics from being reported. The method always returns aDatabaseHealthobject — it never throws.[ADD]
src/database/transaction.runner.ts— Transaction helper (110 lines)A reusable, dependency-injectable service for atomic database operations:
Usage example
Design guarantees
START TRANSACTION→ callback →COMMITon success /ROLLBACKon errorfinally { queryRunner.release() }— the connection is ALWAYS returned to the pool, even if the callback throwsrunNested()uses PostgreSQL savepoints (SAVEPOINT/ROLLBACK TO SAVEPOINT) for sub-transaction isolationcallback(manager)receives anEntityManager— all repos MUST use this manager to participate in the transaction[ADD]
src/database/base.repository.ts— Repository base class (105 lines)A generic, type-safe base class that all domain repositories should extend:
Key design features
withManager(manager: EntityManager): this— Returns a repository instance bound to a specific EntityManager. This is the transaction-scoping mechanism:Without
withManager(), callingrepo.update()inside a transaction would use the global DataSource (outside the transaction boundary) and not be rolled back on failure.Consistent API surface. All repositories that extend
BaseRepositoryshare the same method signatures, making the codebase predictable and reducing the learning curve for new contributors.[ADD]
src/database/index.ts— Barrel exports (6 lines)[MODIFY]
src/config/data-source.ts— CLI PostgreSQL support (+53/-21)The CLI data source (used by
typeorm migration:generate,migration:run,migration:revert) now supports PostgreSQL:DATABASE_URLis set → PostgreSQL with connection pooling, SSL, and the same pool configuration as the NestJS moduleDATABASE_URLis not set → SQLite fallback (preserving existing development workflow)synchronize: falseenforced for CLI usage (migrations are the only schema management path)DB_POOL_MAX,DB_POOL_IDLE_TIMEOUT,DB_POOL_ACQUIRE_TIMEOUT) are read from the same env vars for consistencyMigration commands continue to work unchanged:
[MODIFY]
src/app.module.ts— Use DatabaseModule (+3/-16)TypeOrmModule.forRoot({ type: 'sqlite', database: 'database.sqlite', ... })blockimport { DatabaseModule } from './database/database.module'andDatabaseModulein theimportsarrayDatabaseModuleis@Global()so all existing modules (AuthModule,ClaimsModule,RewardsModule, etc.) can injectTransactionRunnerandDatabaseServicewithout explicit importsTypeOrmModuleis still exported fromDatabaseModule, so existingTypeOrmModule.forFeature([...])usage in feature modules continues to work unchangedFiles Changed
src/database/database.module.tssrc/database/database.service.tssrc/database/transaction.runner.tssrc/database/base.repository.tswithManager()src/database/index.tssrc/config/data-source.tssrc/app.module.tsEnvironment Variables Reference
DATABASE_URLDB_HOSTlocalhostDATABASE_URLis not set)DB_PORT5432DB_USERNAMEpostgresDB_PASSWORDDB_DATABASEtruthbountyDB_SSLfalsetruefor cloud providers)DB_POOL_MAX20DB_POOL_IDLE_TIMEOUT30000DB_POOL_ACQUIRE_TIMEOUT60000DATABASE_SYNCHRONIZEfalseDATABASE_LOGGINGfalseVerification
TypeScript / NestJS compilation
✅ Compiles without errors. The
DatabaseModuleis a standard NestJS@Global()module with no exotic imports or configuration.Existing health module compatibility
The existing
HealthService(src/health/health.service.ts) includes acheckDatabase()method that callsthis.dataSource.query('SELECT 1'). TheDataSourceinjected intoHealthServiceis the same one configured byDatabaseModule. No changes to the health module were needed — database health checks continue to work transparently.Migration CLI compatibility
The
package.jsonscripts referencesrc/config/data-source.ts:All three commands work with both PostgreSQL and SQLite based on the environment configuration in the updated
data-source.ts.Preexisting test failures
The upstream repository has 17 failing test suites out of 88 total (616/629 individual tests pass). The failures are in unrelated areas: mock setup mismatches, entity resolution issues in
@nestjs/testing, and module import ordering problems. None of these failures are in the newdatabase/module or the modifiedapp.module.ts. These preexisting failures are explicitly not addressed here to keep the PR focused on the PostgreSQL infrastructure.Acceptance Criteria
TypeOrmModule.forRootAsyncwithDATABASE_URLor individual vars__dirname + '/../**/*.entity{.ts,.js}'BaseRepository<T>withwithManager()for transaction scopingdata-source.tssupports PostgreSQL + SQLiteTransactionRunnerwithROLLBACKon errorextra: { max, idleTimeoutMillis, connectionTimeoutMillis }TransactionRunner.run()with commit/rollback lifecycleDatabaseService.getHealth()with connectivity, latency, pool statsDATABASE_SYNCHRONIZErequires explicit opt-inssl: { rejectUnauthorized: false }whenDB_SSL=trueDATABASE_URLis set, falls back to SQLite automaticallyOut of Scope (intentional)
src/scripts/seed.tsalready exists in the repo) should be updated to use theTransactionRunnerin a follow-up PR.BaseRepository<T>provides the infrastructure. Individual domain repositories (UserRepository,BountyRepository, etc.) should extend it in their respective module PRs.DatabaseService.getHealth()provides the data. Grafana/Datadog dashboard configuration is infrastructure-as-code and outside this PR's scope.