Aviary
Channels

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-database
npm install @dudousxd/nestjs-notifications-database

Register the channel

app.module.ts
import { DatabaseChannelModule } from '@dudousxd/nestjs-notifications-database';

@Module({
  imports: [
    DatabaseChannelModule.forRoot(), // bundled in-memory store
  ],
})
export class AppModule {}

DatabaseChannelModule.forRoot() takes:

OptionTypeDefaultDescription
storeType<NotificationStore>InMemoryStoreStore class to instantiate.
importsDynamicModule['imports'][]Extra modules to import (e.g. the ORM module the store depends on).
globalbooleantrueRegister 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:

invoice-paid.notification.ts
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:

  1. routeNotificationFor('database') may return a { type, id } reference directly.
  2. Otherwise it calls the notifiable's toNotifiableRef().

If neither is available it throws, asking you to provide one:

user.ts
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:

MethodReturnsDescription
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 typeorm
npm install @dudousxd/nestjs-notifications-database-typeorm @nestjs/typeorm typeorm

Add the adapter's NotificationEntity to your TypeOrmModule.forRoot() entities (or migrations), then wire the store and channel:

app.module.ts
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/nestjs
npm install @dudousxd/nestjs-notifications-database-mikro-orm @mikro-orm/core @mikro-orm/decorators @mikro-orm/nestjs

Targets 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:

app.module.ts
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-prisma
npm install @dudousxd/nestjs-notifications-database-prisma

Prisma's client is app-owned, so you pass your instance in. Add a Notification model to your schema.prisma:

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():

app.module.ts
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:

app.module.ts
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.

migrations/1700000000000-AddNotifications.ts
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).

migrations/AddNotifications.ts
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-notifications

On this page