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.

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.

@stratal/framework registers database and migration commands with the Quarry CLI. Use the --schema flag to target each connection’s schema independently.

Terminal window
# Generate ZenStack ORM client
npx quarry db:generate
# Generate with watch mode for development
npx quarry db:generate --watch
# Introspect an existing database and generate schema
npx quarry db:pull --schema db/main/schema.zmodel
# Push schema changes to the database (skips migration history)
npx quarry db:push --schema db/main/schema.zmodel
CommandOptionsDescription
db:generate--schema, --watchGenerate ZenStack ORM client
db:pull--schemaIntrospect database and generate schema
db:push--schema, --accept-data-loss, --force-resetPush schema changes to database
Terminal window
# Create and apply a migration
npx quarry migrate:dev --schema db/main/schema.zmodel --name add_users_table
# Create migration without applying
npx quarry migrate:dev --schema db/main/schema.zmodel --name add_users_table --create-only
# Deploy pending migrations to production
npx quarry migrate:deploy --schema db/main/schema.zmodel
# Check migration status
npx quarry migrate:status --schema db/main/schema.zmodel
# Reset database (destructive)
npx quarry migrate:reset --schema db/main/schema.zmodel --force
CommandOptionsDescription
migrate:dev--schema, --name, --create-onlyCreate and apply a migration
migrate:deploy--schemaDeploy pending migrations
migrate:status--schemaCheck migration status
migrate:reset--schema, --force, --skip-seedReset database