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 uses the notification class's static notificationName if set, otherwise its
class name.
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) | Promise<StoredNotification[]> | All notifications for a notifiable, newest first. |
getUnread(notifiableType, notifiableId) | Promise<StoredNotification[]> | Unread notifications for a notifiable. |
delete(id) | Promise<void> | Remove one notification. |
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-notificationsSend 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.