Database
Persist notifications so you can show an in-app feed. A NotificationStore interface, a bundled in-memory store, and TypeORM / MikroORM / Prisma adapters.
The database channel writes a notification to a store so you can render an in-app feed — an unread
count, a notifications dropdown, a "mark all as read" button. It persists through a
NotificationStore abstraction: a bundled in-memory store for development, and ORM adapters
(TypeORM, MikroORM, Prisma) for production. The row shape mirrors Laravel's notifications table.
Install
pnpm add @dudousxd/nestjs-notifications-databasenpm install @dudousxd/nestjs-notifications-databaseRegister the channel
import { DatabaseChannelModule } from '@dudousxd/nestjs-notifications-database';
@Module({
imports: [
DatabaseChannelModule.forRoot(), // bundled in-memory store
],
})
export class AppModule {}DatabaseChannelModule.forRoot() takes:
| Option | Type | Default | Description |
|---|---|---|---|
store | Type<NotificationStore> | InMemoryStore | Store class to instantiate. |
imports | DynamicModule['imports'] | [] | Extra modules to import (e.g. the ORM module the store depends on). |
global | boolean | true | Register globally so the channel is discoverable app-wide. |
There's also DatabaseChannelModule.forFeature(), which registers only the channel and reuses a
NOTIFICATION_STORE token provided elsewhere — that's how the ORM adapter modules below wire in.
The notification side
Annotate the payload method with the @Database() handle and return a plain object — that object
becomes the persisted data:
import { type Notifiable, Notification } from '@dudousxd/nestjs-notifications-core';
import { Database } from '@dudousxd/nestjs-notifications-database';
@Notification()
export class InvoicePaid {
constructor(private invoiceId: string, private amount: number) {}
@Database()
toDatabase({ notifiable }: ChannelContext): Record<string, unknown> {
return { invoiceId: this.invoiceId, amount: this.amount };
}
}The Database handle also works as a via() token (via() { return [Database]; }) for
explicit routing. For
compile-time checks, implement DatabaseNotification alongside the decorator.
toDatabase() is optional when you route with an explicit via(). If it's absent the channel falls
back to toArray() (Laravel parity), and finally to a structural copy of the notification's own
properties.
The notifiable reference
Persisting needs to know whose notification this is, as a { type, id } pair. The channel resolves
it in two ways:
routeNotificationFor('database')may return a{ type, id }reference directly.- Otherwise it calls the notifiable's
toNotifiableRef().
If neither is available it throws, asking you to provide one:
export class User implements Notifiable {
constructor(public id: number) {}
routeNotificationFor() {
return undefined;
}
toNotifiableRef() {
return { type: 'User', id: this.id };
}
}Stored shape
Each persisted notification is a StoredNotification:
interface StoredNotification {
id: string;
type: string; // notification class name, e.g. "InvoicePaid"
notifiableType: string; // reference type, e.g. "User"
notifiableId: string; // reference id
data: Record<string, unknown>; // what toDatabase() returned
readAt: Date | null;
createdAt: Date;
updatedAt: Date;
}The stored type is the notification's notificationName(notification) — an instance-level
notificationType() override when implemented, else the class-level @Notification({ name }),
else the class name. See notificationType()
for when to reach for the instance override (e.g. one generic notification class carrying many
event names). This is also the value the ?type= inbox filter
matches against.
The store
A store implements NotificationStore. These are the methods you get for reading and managing a
feed:
| Method | Returns | Description |
|---|---|---|
save(notification) | Promise<StoredNotification> | Persist a new row (the channel calls this). |
markAsRead(id) | Promise<void> | Mark one notification read. |
markAllAsRead(notifiableType, notifiableId) | Promise<void> | Mark every unread row for a notifiable read. |
getForNotifiable(notifiableType, notifiableId, types?) | Promise<StoredNotification[]> | All notifications for a notifiable, newest first. Optional types: string[] restricts to matching type values. |
getUnread(notifiableType, notifiableId, types?) | Promise<StoredNotification[]> | Unread notifications for a notifiable. Same optional types filter. |
delete(id) | Promise<void> | Remove one notification. |
deleteOwned(id, owner) | Promise<boolean> | Optional. Remove one notification only when it belongs to owner ({ notifiableType, notifiableId, tenantId? }); true when a row matched. |
markAsReadOwned(id, owner) | Promise<boolean> | Optional. Mark one read only when it belongs to owner; true when a row matched, including an already-read one. |
findById(id) | Promise<StoredNotification | null> | Optional. Fetch one row — the ownership fallback when the two scoped mutations are absent. |
types is absent or an empty array by default, matching every type (unchanged behavior) — it's
purely additive. PaginateForNotifiableOptions carries the same types?: string[] for the
paginated read path. Every bundled adapter (in-memory, TypeORM, MikroORM, Prisma) implements the
filter; see In-app notifications for the higher-level
NotificationsQueryService and REST ?type= query param this powers.
The store is bound to the NOTIFICATION_STORE token, so inject it to build your feed endpoints:
import { Inject } from '@nestjs/common';
import { NOTIFICATION_STORE, type NotificationStore } from '@dudousxd/nestjs-notifications-database';
constructor(@Inject(NOTIFICATION_STORE) private store: NotificationStore) {}The bundled InMemoryStore keeps rows in a Map. It's perfect for tests and prototyping — but it's
not durable, so use a persistence adapter in production.
Reading notifications in-app
You usually don't inject the raw store. The NotificationsQueryService (provided by the database
module) wraps it with the read/manage methods you reach for — all, unread, unreadCount,
paginate, markAsRead, markAllAsRead, delete — accepting a notifiable or a { type, id } ref.
There's also an optional REST controller you can mount in one call.
See In-app notifications for the full recipe.
Persistence adapters
For real persistence, pair the channel with an adapter. Each adapter provides a store bound to the
NOTIFICATION_STORE token, plus the schema it needs (a NotificationEntity for the ORM adapters, a
Notification model for Prisma). You register the adapter's store module alongside
DatabaseChannelModule.forFeature() — the adapter provides the store token, forFeature() registers
the channel that consumes it.
pnpm add @dudousxd/nestjs-notifications-database-typeorm @nestjs/typeorm typeormnpm install @dudousxd/nestjs-notifications-database-typeorm @nestjs/typeorm typeormAdd the adapter's NotificationEntity to your TypeOrmModule.forRoot() entities (or migrations),
then wire the store and channel:
import { DatabaseChannelModule } from '@dudousxd/nestjs-notifications-database';
import { TypeOrmNotificationStoreModule } from '@dudousxd/nestjs-notifications-database-typeorm';
@Module({
imports: [
// ...TypeOrmModule.forRoot({ entities: [NotificationEntity, ...] })
TypeOrmNotificationStoreModule.forFeature(),
DatabaseChannelModule.forFeature(),
],
})
export class AppModule {}TypeOrmNotificationStoreModule.forFeature() registers TypeOrmModule.forFeature([NotificationEntity])
internally and binds TypeOrmNotificationStore to NOTIFICATION_STORE. The entity (NotificationEntity)
is exported if you'd rather register it yourself.
pnpm add @dudousxd/nestjs-notifications-database-mikro-orm @mikro-orm/core @mikro-orm/decorators @mikro-orm/nestjsnpm install @dudousxd/nestjs-notifications-database-mikro-orm @mikro-orm/core @mikro-orm/decorators @mikro-orm/nestjsTargets MikroORM v7 (peers ^7). It needs @mikro-orm/decorators because v7 moved the
decorators out of @mikro-orm/core; the adapter's entity uses the legacy (TypeScript
experimental) decorators, matching the rest of a NestJS app.
Register the adapter's NotificationEntity with MikroORM, then wire the store and channel:
import { DatabaseChannelModule } from '@dudousxd/nestjs-notifications-database';
import { MikroOrmNotificationStoreModule } from '@dudousxd/nestjs-notifications-database-mikro-orm';
@Module({
imports: [
// ...MikroOrmModule.forRoot({ entities: [NotificationEntity, ...] })
MikroOrmNotificationStoreModule.forFeature(),
DatabaseChannelModule.forFeature(),
],
})
export class AppModule {}MikroOrmNotificationStoreModule.forFeature() registers MikroOrmModule.forFeature([NotificationEntity])
and binds MikroOrmNotificationStore to NOTIFICATION_STORE.
pnpm add @dudousxd/nestjs-notifications-database-prismanpm install @dudousxd/nestjs-notifications-database-prismaPrisma's client is app-owned, so you pass your instance in. Add a Notification model to your
schema.prisma:
model Notification {
id String @id
type String
notifiableType String
notifiableId String
data Json
readAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}A real PrismaClient structurally satisfies the adapter's client type, so pass it directly to
forRoot():
import { DatabaseChannelModule } from '@dudousxd/nestjs-notifications-database';
import { PrismaNotificationStoreModule } from '@dudousxd/nestjs-notifications-database-prisma';
@Module({
imports: [
PrismaNotificationStoreModule.forRoot({ client: prisma }),
DatabaseChannelModule.forFeature(),
],
})
export class AppModule {}PrismaNotificationStoreModule.forRoot({ client }) binds the client to the PRISMA_CLIENT token
and PrismaNotificationStore to NOTIFICATION_STORE. If you already provide PRISMA_CLIENT
elsewhere (e.g. a shared PrismaModule), use PrismaNotificationStoreModule.forFeature() instead.
Neither adapter fits your schema? Implement NotificationStore yourself and pass it as the store
option to DatabaseChannelModule.forRoot(). The interface is six methods.
Schema
By default the channel is self-contained: on boot it creates the notifications table if it's
missing, non-destructively — it only adds what's absent and never drops anything. That's
autoCreateSchema: true, the default on both DatabaseChannelModule.forRoot() and
DatabaseChannelModule.forFeature(). Install an adapter, register the module, and the feed works on
first run with no migration step.
Auto-create is non-destructive — it only creates missing tables and columns, so it's safe to leave
on. Turn it off (autoCreateSchema: false) for strict, migration-only environments where the app
must never touch DDL.
To own the schema with your ORM's migrations instead, disable auto-create and use the exported migration helpers:
DatabaseChannelModule.forFeature({ autoCreateSchema: false }),Call createNotificationsTable(queryRunner) inside a TypeORM migration. It's non-destructive
(creates the table only if it doesn't exist) and driver-portable — the column types come from the
entity metadata.
import { createNotificationsTable } from '@dudousxd/nestjs-notifications-database-typeorm';
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class AddNotifications1700000000000 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await createNotificationsTable(queryRunner);
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('notifications', true);
}
}Get the non-destructive SQL with notificationsSchemaSql(em) and add it to a MikroORM
Migration. It returns only the statements needed to bring the notifications table up to date
(create it, add missing columns).
import { notificationsSchemaSql } from '@dudousxd/nestjs-notifications-database-mikro-orm';
import { Migration } from '@mikro-orm/migrations';
export class AddNotifications extends Migration {
async up(): Promise<void> {
this.addSql(await notificationsSchemaSql(this.getEntityManager()));
}
}Prisma is schema-first, so autoCreateSchema is a no-op for the Prisma adapter — it won't run
DDL. Add the Notification model to your schema.prisma and apply it
with prisma migrate:
npx prisma migrate dev --name add-notificationsManaged tables (schema-diff tooling)
The MikroORM and TypeORM adapters export notificationsManagedTables() — the table name(s) this
store creates/manages (['notifications']), derived from the same set ensureSchema() scopes its
fingerprint to. Feed it to your ORM's schema-diff exclude list (e.g. MikroORM's
schemaGenerator.skipTables) so a consumer never hand-maintains this table name separately —
mirroring durableManagedTables() / telescopeManagedTables() from the sibling @dudousxd
ecosystem libs.
import { notificationsManagedTables } from '@dudousxd/nestjs-notifications-database-mikro-orm';
schemaGenerator: { skipTables: notificationsManagedTables() },Send email notifications. A fluent MailMessage builder, an HTML + text renderer, and a swappable transport — nodemailer SMTP out of the box.
Broadcast
Push notifications to the browser in real time over socket.io. Each notifiable gets its own room; pair it with the database channel for a live in-app feed.