Skip to content

Database

@stratal/framework provides a database module built on ZenStack ORM. It supports multiple named connections, per-connection schemas, and plugins for error handling, event emission, and schema switching.

Terminal window
yarn add @stratal/framework

Use DatabaseModule.forRoot() to configure your database connections:

import { Module } from 'stratal/module'
import { DatabaseModule } from '@stratal/framework/database'
import { schema } from '../db/schema'
@Module({
imports: [
DatabaseModule.forRoot({
default: 'main',
connections: [
{
name: 'main',
schema,
dialect: () => new PostgresDialect({ pool }),
},
],
}),
],
})
export class AppModule {}
OptionTypeDescription
defaultstringName of the default connection
connectionsDatabaseConnectionConfig[]Array of connection configurations

Each connection accepts:

OptionTypeDescription
namestringUnique connection name
schemaobjectYour ZenStack schema for this connection
dialect() => DialectFactory function returning a database dialect
pluginsPlugin[]Optional array of database plugins

Inject the default database connection using DI_TOKENS.Database:

import { Transient, inject, DI_TOKENS } from 'stratal/di'
import type { DatabaseService } from '@stratal/framework/database'
@Transient()
export class UsersService {
constructor(
@inject(DI_TOKENS.Database) private readonly db: DatabaseService,
) {}
async findAll() {
return this.db.user.findMany()
}
}

Use @InjectDB(name) to inject a specific named connection:

import { Transient } from 'stratal/di'
import { InjectDB, type DatabaseService } from '@stratal/framework/database'
@Transient()
export class AnalyticsService {
constructor(
@InjectDB('analytics') private readonly db: DatabaseService<'analytics'>,
) {}
async trackEvent(event: string) {
await this.db.analyticsEvent.create({ data: { event } })
}
}

Configure multiple connections by providing each with its own schema. Each connection has an independent .zmodel file containing only the models for that connection.

db/
├── main/
│ └── schema.zmodel
└── analytics/
└── schema.zmodel
import { Module } from 'stratal/module'
import { DatabaseModule } from '@stratal/framework/database'
import { schema as mainSchema } from '../db/main/schema'
import { schema as analyticsSchema } from '../db/analytics/schema'
@Module({
imports: [
DatabaseModule.forRoot({
default: 'main',
connections: [
{
name: 'main',
schema: mainSchema,
dialect: () => new PostgresDialect({ pool: mainPool }),
},
{
name: 'analytics',
schema: analyticsSchema,
dialect: () => new PostgresDialect({ pool: analyticsPool }),
},
],
}),
],
})
export class AppModule {}

With per-connection schemas, DatabaseService<'main'> only exposes models defined in the main schema, while DatabaseService<'analytics'> only exposes models defined in the analytics schema.

Each connection has its own independent .zmodel file — simply define each schema separately.

db/main/schema.zmodel

datasource db {
provider = "postgresql"
url = env("MAIN_DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
author User @relation(fields: [authorId], references: [id])
authorId String
}

db/analytics/schema.zmodel

datasource db {
provider = "postgresql"
url = env("ANALYTICS_DATABASE_URL")
}
model AnalyticsEvent {
id String @id @default(cuid())
event String
createdAt DateTime @default(now())
}
model PageView {
id String @id @default(cuid())
path String
}

The DatabaseModule automatically registers the following plugins on every connection — no configuration needed:

  • ErrorHandlerPlugin — Transforms ZenStack errors into Stratal ApplicationError instances with appropriate HTTP status codes.
  • EventEmitterPlugin — Emits events before and after database operations, integrating with Stratal’s event system. See Database Events for the full event pattern documentation.

The only user-configurable plugin is SchemaSwitcherPlugin, which sets the PostgreSQL search_path for multi-tenant isolation:

import { SchemaSwitcherPlugin } from '@stratal/framework/database'
{
name: 'main',
schema: mainSchema,
dialect: () => new PostgresDialect({ pool }),
plugins: [new SchemaSwitcherPlugin()],
}

To get full type safety across connections, augment the StratalDatabase interface with your per-connection schema types:

import type { MainSchemaType } from '../db/main/schema'
import type { AnalyticsSchemaType } from '../db/analytics/schema'
declare module '@stratal/framework/database' {
interface StratalDatabase {
schemas: {
main: MainSchemaType
analytics: AnalyticsSchemaType
}
defaultConnection: 'main'
}
}

This provides full type safety — DatabaseService returns a client typed with the default connection’s schema, while DatabaseService<'analytics'> returns a client typed with the analytics schema.

Use the standard zenstack CLI with the --schema flag to target each connection’s schema independently.

Terminal window
# Run dev migrations for the main connection
zenstack migrate dev --schema db/main/schema.zmodel --name add_users_table
# Deploy migrations to production
zenstack migrate deploy --schema db/main/schema.zmodel
# Check migration status
zenstack migrate status --schema db/main/schema.zmodel
# Reset database
zenstack migrate reset --schema db/main/schema.zmodel
Terminal window
# Push schema to database (skips migration history)
zenstack db push --schema db/main/schema.zmodel
# Push analytics schema
zenstack db push --schema db/analytics/schema.zmodel
OptionDescription
--schema <path>Path to the connection’s .zmodel schema file
--name <name>Migration name (for migrate dev)