Prisma 7 — Multi-Schema DI Wiring (Main/AI/Logs/Com/Accounting)
Audiences: developer, internal
ddx-api runs five completely separate PostgreSQL databases, each with its own Prisma schema, generated client package, and NestJS service — enforcing data isolation at the connection level rather than at the query filter level.
Business Purpose
Healthcare systems have competing data-residency requirements that cannot be satisfied by a single database:
- Clinical data (patients, visits, prescriptions) must be isolated from AI telemetry (chat sessions, token costs, model usage) to contain blast radius from AI provider incidents.
- Communication logs (emails, SMS, WhatsApp delivery receipts) are subject to different retention laws than clinical records — they need an independent purge schedule.
- Audit and access logs must be write-segregated from business tables so that a compromised business-layer credential cannot retroactively delete audit evidence.
- Accounting data (double-entry journals, fiscal periods, invoice payments) falls under financial regulations (HGB, GDPR financial records) that require its own backup cadence and access controls.
Splitting into five databases satisfies these requirements structurally: a developer injecting only PrismaService (main) cannot reach ddx_api_log even if they try — they simply do not hold a connection to it.
Business outcome → Technical mechanism: A clinic administrator exports a patient billing report. The billing service injects PrismaAccountingService — a dedicated connection to ddx_api_acc — while simultaneously the request is logged by AccessLogInterceptor writing to ddx_api_log via PrismaLoggingService. Both writes happen in parallel with zero coupling between the two clients. The main DB never touches the log row, and the log DB never sees billing data.
Audiences
- Investor: Five isolated databases demonstrate enterprise-grade data hygiene and GDPR/HGB compliance separation — a selling point in regulated EU healthcare markets.
- Clinical buyer (doctor/nurse/receptionist): Transparent — the five-database split never surfaces in the UI. Clinicians interact with a unified system; isolation is an infrastructure concern.
- Developer/partner: High impact. Any new NestJS module that needs to write to a non-main database must inject the correct service (see DI Wiring section). Cross-schema joins are architecturally impossible at the Prisma level; composition must happen in the service layer.
- Internal (ops/support): Each database has an independent backup, migration, and retention policy. Ops must run
pnpm run prisma:migrate:deployto apply all five schemas atomically, or per-schema scripts for surgical updates.
Architecture
The Five Schemas at a Glance
| Schema dir | PostgreSQL DB | NestJS service | Models | Enums | Env var |
|---|---|---|---|---|---|
prisma-main/models/ (20 files) | ddx_api_main | PrismaService | 135 | 104 | DATABASE_URL |
prisma-ai/schema.prisma | ddx_api_ai | PrismaAiService | 31 | 26 | AI_DATABASE_URL |
prisma-logging/schema.prisma | ddx_api_log | PrismaLoggingService | 6 | 0 | LOGGING_DATABASE_URL |
prisma-com/schema.prisma | ddx_api_com | PrismaComService | 7 | 5 | COM_DATABASE_URL |
prisma-accounting/schema.prisma | ddx_api_acc | PrismaAccountingService | 16 | 21 | ACCOUNTING_DATABASE_URL |
A sixth database (ddx_fhir_core) is managed exclusively by HAPI FHIR (Java/Hibernate). Never run Prisma against it — Prisma's migrate deploy will DROP all HAPI tables.
Main Schema — Split-File Layout
Unlike the other four schemas (single .prisma files), the main schema is split across 20 model files under ddx-api/prisma-main/models/. Prisma's multi-file schema feature (stable in Prisma 6+) compiles these into one logical schema at generate time. The generator output lands in node_modules/@ddx/prisma-main — the same import path that PrismaService extends.
DI Module Topology
AppModule
└── PrismaModule (@Global) ← ddx_api_main + ddx_api_ai + ddx_api_com + ddx_api_acc
PrismaService → ddx_api_main
PrismaAiService → ddx_api_ai
PrismaComService → ddx_api_com
PrismaAccountingService → ddx_api_acc
└── LoggingModule (@Global) ← ddx_api_log ONLY
PrismaLoggingService → ddx_api_log
PrismaModule is @Global(), so any module in the application can inject PrismaService, PrismaAiService, PrismaComService, or PrismaAccountingService directly — no local imports needed.
PrismaLoggingService is intentionally excluded from PrismaModule. It is registered in LoggingModule (@Global()), which exports it alongside LoggingService. Business services must never inject PrismaLoggingService directly — they must go through LoggingService or AuditService. This prevents business logic from writing arbitrary data to the audit trail.
Reference: ddx-api/src/platform/prisma/prisma.module.ts:18-37 (comment block explains the exclusion explicitly).
Service Class Pattern
Every Prisma service follows the same pattern:
@Injectable()
export class PrismaXxxService
extends PrismaClient // extends the generated client
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() { await this.$connect(); }
async onModuleDestroy() { await this.$disconnect(); }
}
The generated client for each schema is a separate npm package (@ddx/prisma-main, @ddx/prisma-ai, etc.) published into node_modules by the local output directive in each generator client {} block. NestJS's DI container manages the connection lifecycle via OnModuleInit / OnModuleDestroy.
Static connectedLogged flag: All five services carry a private static connectedLogged = false field. NestJS may instantiate the same @Injectable() 2-3 times during module initialization when a service is re-provided in multiple modules. The static flag ensures only one "connected" log line appears per process, preventing log noise from tripping monitoring alerts.
Tech Stack & Choices
Prisma version in use
ddx-api/package.json lists prisma ^6.19.1. The project documentation and agent config refer to "Prisma 7" as a forward-looking target. The patterns in place (multi-file schema, per-schema clients, datasources constructor override, $connect/$disconnect lifecycle) are compatible with both Prisma 6 and 7. The migration path from 6 to 7 is additive — the main breaking change in Prisma 7 is the shift to prisma.config.ts as the primary configuration file (replacing generator and datasource blocks inline in the schema), plus stable driver adapters (bypassing the Rust query engine for environments like Cloudflare Workers or edge runtimes). Neither driver adapters nor prisma.config.ts are currently deployed in ddx-api; the project uses standard Node.js Prisma with the Rust query engine via the Postgres native adapter.
Why five separate clients instead of one with schemas?
Prisma's multi-schema feature (PostgreSQL SET search_path) allows one client to span multiple schemas within one PostgreSQL instance. The project chose separate PostgreSQL instances (see ddx-api/CLAUDE.md database table) rather than one instance with schemas. This means:
- Each database can run on a separate host, separate backup policy, and separate connection pool.
- A connection-level credential separation is possible (different DB users per service).
- A schema migration on
ddx_api_aicannot blockddx_api_mainmigrations.
Generated client output convention
Each generator client {} block in the schema files targets output = "../node_modules/@ddx/prisma-<name>":
ddx-api/prisma-ai/schema.prisma:8—output = "../node_modules/@ddx/prisma-ai"ddx-api/prisma-logging/schema.prisma:4—output = "../node_modules/@ddx/prisma-logging"ddx-api/prisma-com/schema.prisma:4—output = "../node_modules/@ddx/prisma-com"ddx-api/prisma-accounting/schema.prisma:9—output = "../node_modules/@ddx/prisma-accounting"
The main schema uses output = "../node_modules/@ddx/prisma-main" (declared in one of its 20 model files). Services import from these package aliases, e.g. import { PrismaClient } from '@ddx/prisma-main' (ddx-api/src/platform/prisma/prisma.service.ts:7).
URL injection — constructor vs datasources override
PrismaService (main) reads DATABASE_URL from the environment implicitly via the datasource db { url = env("DATABASE_URL") } directive in the schema. The four domain-specific services (AI, Com, Logging, Accounting) use an explicit datasources constructor override reading from ConfigService, e.g.:
// ddx-api/src/ai/ai-core/prisma-ai.service.ts:27-35
constructor(private readonly configService: ConfigService) {
super({
datasources: {
db: { url: configService.get<string>('AI_DATABASE_URL') },
},
log: ['error'],
});
}
This lets NestJS's ConfigService validate the env variable at boot (via validate-env.ts) before the first connection attempt.
prisma.config.ts — Prisma 7 feature not yet deployed
Prisma 7 introduces prisma.config.ts as a first-class configuration file (analogous to drizzle.config.ts) that centralises migrate, generate, and studio settings. As of last_verified_date, no prisma.config.ts exists in ddx-api/. Configuration is expressed via per-schema generator + datasource blocks and the package.json scripts documented below.
TypedSQL and Client Extensions
TypedSQL (Prisma 6+) allows raw SQL files in a sql/ directory to be compiled into fully-typed query functions. Not currently used in ddx-api — raw queries are expressed via this.$queryRaw template literals in the ping() methods.
Client Extensions (replacing the deprecated $use middleware from Prisma 5) allow adding custom methods or result transformers to the client. Not currently used. The $use pattern is absent from all five service files; the project relies on NestJS interceptors (e.g. AuditInterceptor) for cross-cutting concerns rather than Prisma middleware.
Data Flow
Standard request path (business data)
HTTP Request
└─► Controller (e.g. BillingController)
└─► Service (e.g. BillingService)
└─► inject PrismaAccountingService ← ddx_api_acc
└─► this.payment.create(...) ← SQL INSERT into payments table
└─► inject PrismaService ← ddx_api_main (if patient ref needed)
└─► this.patient.findUnique(...)
Cross-schema: if BillingService needs to read a patient name from ddx_api_main while writing a payment to ddx_api_acc, it holds two injected services. There is no Prisma-level join across these two databases. The service fetches from each client in sequence or parallel and assembles the result in-process.
AI session flow
TucanController → TucanService → PrismaAiService (ddx_api_ai) chatSession.create(...) chatMessage.create(...) tucanUsageRecord.create(...) ← cost tracking
The AI DB stores no clinical PII — only session IDs, token counts, and model names. Clinical context (patient ID, visit ID) appears as opaque UUID columns in ChatSession with no FK constraints (no cross-DB FK in PostgreSQL across separate instances).
Logging path (decoupled)
Any inbound HTTP request
└─► AccessLogInterceptor (global interceptor #5 in the pipeline)
└─► LoggingService.log(...)
└─► PrismaLoggingService (ddx_api_log)
└─► accessLog.create(...)
This path is completely decoupled from business services. PrismaLoggingService is NOT available for injection in business modules — only LoggingService (which wraps it) is exported from LoggingModule.
Communication dispatch flow
NotificationService → PrismaComService (ddx_api_com) communicationLog.create(status: PENDING) communicationQueue.create(scheduledAt: ...) [cron or webhook callback] communicationLog.update(status: SENT | FAILED) communicationEvent.create(...)
Implicated Code
Schema files
ddx-api/prisma-ai/schema.prisma:1-14— generator + datasource block; databaseddx_clinic_fhir_ai, envAI_DATABASE_URL, output@ddx/prisma-aiddx-api/prisma-logging/schema.prisma:1-13— generator + datasource block; databaseddx_api_log, envLOGGING_DATABASE_URL, output@ddx/prisma-logging; 6 models, 0 enums (append-only, no status enums)ddx-api/prisma-com/schema.prisma:1-13— generator + datasource block; databaseddx_api_com, envCOM_DATABASE_URL, output@ddx/prisma-comddx-api/prisma-accounting/schema.prisma:1-15— generator + datasource block; databaseddx_api_acc, envACCOUNTING_DATABASE_URL, output@ddx/prisma-accounting
Service files
ddx-api/src/platform/prisma/prisma.service.ts:1-105—PrismaServiceextendsPrismaClientfrom@ddx/prisma-main;static connectedLoggedflag at line 23;ping()method at line 66 for health checksddx-api/src/ai/ai-core/prisma-ai.service.ts:1-72—PrismaAiServiceextendsPrismaAiClientfrom@ddx/prisma-ai; readsAI_DATABASE_URLviaConfigService(lines 27-35);static connectedLoggedat line 24ddx-api/src/communication/communication-core/prisma-com.service.ts:1-186—PrismaComService; includes domain helperscleanupOldCommunications()(line 83) andgetOrganizationStats()(line 146) — service-layer logic co-located with the connection managerddx-api/src/platform/logging/prisma-logging.service.ts:1-102—PrismaLoggingServiceextendsLoggingPrismaClient;cleanupOldLogs()at line 73 readsLOG_RETENTION_DAYS(default 90 days)ddx-api/src/financial/accounting/prisma-accounting/prisma-accounting.service.ts:1-94—PrismaAccountingService; exportsAccountingTransactionClienttype alias at line 11 for typed$transactioncallbacks;executeTransaction<T>()helper at line 87
Module wiring
ddx-api/src/platform/prisma/prisma.module.ts:1-37—@Global() PrismaModule; registersPrismaService,PrismaAiService,PrismaComService,PrismaAccountingService; comment at line 19 explicitly documents thatPrismaLoggingServiceis excluded ("scoped by design — only via LoggingModule/AuditService")ddx-api/src/platform/logging/logging.module.ts:1-34—@Global() LoggingModule; registers and exportsPrismaLoggingServicealongsideLoggingService; the only module allowed to expose the logging client
package.json scripts
ddx-api/package.json— per-schema generate scripts:prisma:generate(main),prisma:generate:ai,prisma:generate:logging,prisma:generate:com,prisma:generate:accounting, plusprisma:generate:all(runsbash scripts/generate-all.sh). Migration equivalents:prisma:migrate,prisma:migrate:all,prisma:migrate:deploy.
Operational Notes
prisma generate — types only, not schema
Running pnpm run prisma:generate:all (or any per-schema variant) only regenerates the TypeScript types in node_modules/@ddx/prisma-<name>. It does NOT:
- Create or alter any PostgreSQL table.
- Apply pending migrations.
- Reflect schema changes made after the last migration.
After adding a model or enum to a .prisma file, the workflow is:
- Write the model/enum in the schema file.
pnpm run prisma:migrate(main) orprisma migrate dev --schema=./prisma-<name>/schema.prismato create the SQL migration file.- Commit the migration file under
prisma-main/models/migrations/(or the equivalent directory for the other schemas). - On production:
pnpm run prisma:migrate:deployapplies all pending migrations across all five databases. pnpm run prisma:generate:allregenerates types (runs automatically as aprebuildhook).
Project-memory pitfall: prisma generate is also triggered automatically as prebuild and prestart hooks (ddx-api/package.json), so the types are always current when the app starts. But if a developer adds a new column and forgets to write a migration, the TypeScript code will compile and the app will boot — then fail at runtime with column does not exist because the DB schema lags behind the generated types.
Schema/enum changes need hand-written SQL when using migrate deploy
prisma migrate deploy only applies files already in the migrations/ directory. For production, the developer must first run prisma migrate dev locally (which generates the SQL migration file), commit it, and then deploy. Skipping migrate dev and manually editing the DB directly will cause prisma migrate status to report "database schema is not in sync" and subsequent migrate deploy runs may fail.
The main schema uses a symlinked migrations directory
ddx-api/prisma-main/models/migrations is a symlink to ddx-api/prisma-main/migrations (auto-created by project tooling). This allows Prisma's --schema=./prisma-main/models flag to find the migrations directory correctly when the schema is spread across 20 files in a subdirectory.
Per-database retention and backup
Each database has an independent retention policy enforced by its service:
| Database | Retention config | Implementation |
|---|---|---|
ddx_api_log | LOG_RETENTION_DAYS (default 90) | PrismaLoggingService.cleanupOldLogs() |
ddx_api_com | COMMUNICATION_RETENTION_DAYS (default 365) | PrismaComService.cleanupOldCommunications() |
ddx_api_main, ddx_api_ai, ddx_api_acc | No in-app purge — external backup/archival | Managed by prisma:backup script |
Health check integration
Every service exposes a ping(): Promise<boolean> method (this.$queryRawSELECT 1``). The health/ NestJS module aggregates these pings for the /health endpoint. A failed ping for any of the five databases returns a degraded status.
Cross-schema data composition — no Prisma join
Because the five schemas live in separate PostgreSQL instances, there is no mechanism for a Prisma query to join across them. Service-layer composition is the only option:
// Example: billing report needing both patient name and payment record
const payment = await this.prismaAcc.payment.findUnique({ where: { id } });
const patient = await this.prismaMain.patient.findUnique({ where: { id: payment.payerId } });
return { ...payment, patientName: patient.fullName };
This is not a limitation but an explicit design constraint: it prevents silent data-mixing and makes cross-domain reads visible in code review.
Never run Prisma against ddx_fhir_core
The HAPI FHIR database (ddx_fhir_core, port 18080) is managed entirely by Java/Hibernate. Running prisma migrate dev against its connection string will DROP all HAPI-managed tables. There is no Prisma schema for it — access is exclusively via the FHIR REST API (FhirClientService).
Related Topics
- PostgreSQL infrastructure — connection pooling, backup strategy, per-database PostgreSQL configuration
- FHIR Resource Facade — how the Prisma main DB and HAPI FHIR are kept in sync via the ResourceFacade pattern
- Audit and Access Logging — how
AuditInterceptorandAccessLogInterceptorwrite toddx_api_logviaPrismaLoggingService - AI Chat Sessions / TUCAN — domain model for
ddx_api_aichat sessions, agent runs, and usage records - Accounting module — double-entry ledger, fiscal periods, payment reconciliation stored in
ddx_api_acc - Full schema reference:
docs/claude-context/CLAUDE_DATABASE.md— authoritative model/enum count per schema, migration commands, connection string patterns