Aviary
Recipes

In-app notifications

Build an in-app feed from persisted notifications. Inject NotificationsQueryService to list, count, and mark read — or mount the optional REST controller in one call.

Once the database channel persists notifications, you render them in-app — an unread badge, a dropdown, a "mark all as read" button. NotificationsQueryService is the read side: it wraps the store with the methods you actually reach for. Mount the bundled REST controller and you have an inbox API with zero glue code.

Inject the query service

NotificationsQueryService is provided by DatabaseChannelModule (and its forFeature()), so it's injectable anywhere the module is in scope:

inbox.service.ts
import { Injectable } from '@nestjs/common';
import { NotificationsQueryService } from '@dudousxd/nestjs-notifications-database';

@Injectable()
export class InboxService {
  constructor(private readonly notifications: NotificationsQueryService) {}

  async feed(user: User) {
    const items = await this.notifications.all(user);
    const badge = await this.notifications.unreadCount(user);
    return { items, badge };
  }
}

Every method that reads "whose notifications" accepts a target: either a notifiable (anything with toNotifiableRef()) or a plain { type, id } reference.

MethodReturnsDescription
all(target)StoredNotification[]Every notification for the target, newest first.
unread(target)StoredNotification[]Unread notifications for the target.
unreadCount(target)numberHow many are unread.
paginate(target, { page?, perPage? }){ items, meta: { page, perPage, total, lastPage } }One page over all (defaults: page 1, 20 per page).
markAsRead(id)voidMark one notification read, by its id.
markAllAsRead(target)voidMark every notification for the target read.
delete(id)voidRemove one notification, by its id.

markAsRead(id) and delete(id) take a notification id (a string), not a target — they act on a single row. markAllAsRead(target) takes the target, since it spans every row for that notifiable.

Pass a ref directly when you only have an id at hand — handy in a controller where the user comes off the request:

await this.notifications.unreadCount({ type: 'User', id: req.user.id });

The REST controller

DatabaseChannelModule.forRoot() auto-mounts the inbox controller by default — you get GET/POST/DELETE /notifications out of the box, with the current notifiable resolved from req.user ({ type: 'User', id: req.user.id }). Customize the resolver, or turn it off:

// Default: controller on, resolveRef reads req.user.
DatabaseChannelModule.forRoot();

// Custom resolver (e.g. a different id field or notifiable type):
DatabaseChannelModule.forRoot({
  controller: { resolveRef: (req) => ({ type: 'User', id: req.auth.sub }) },
});

// Auth guard + a custom base path (avoids colliding with a `/notifications` page
// route under a shared global prefix):
DatabaseChannelModule.forRoot({
  controller: {
    resolveRef: (req) => ({ type: 'User', id: req.user.id }),
    guards: [AuthGuard],
    path: 'notifications-inbox',
  },
});

// Off — mount it yourself, or expose your own endpoints:
DatabaseChannelModule.forRoot({ controller: false });

Mounting it yourself

When you pass controller: false (or want it in a specific module), createNotificationsController() builds the same @Controller('notifications'). Tell it how to resolve the current notifiable from the request, then add the returned class to a module's controllers:

inbox.module.ts
import { Module } from '@nestjs/common';
import {
  DatabaseChannelModule,
  createNotificationsController,
} from '@dudousxd/nestjs-notifications-database';

const NotificationsController = createNotificationsController({
  resolveRef: (req) => ({ type: 'User', id: req.user.id }),
});

@Module({
  imports: [DatabaseChannelModule.forFeature()],
  controllers: [NotificationsController],
})
export class InboxModule {}

resolveRef receives the request and returns a { type, id } ref (it may be async) — wire it to whatever your auth layer puts on req.user. The controller exposes:

RouteAction
GET /notificationsPaginated list (?page & ?perPage).
GET /notifications/unreadUnread notifications.
GET /notifications/unread/count{ count } of unread.
POST /notifications/:id/readMark one read.
POST /notifications/read-allMark all read for the current user.
DELETE /notifications/:idDelete one.

The controller relies on resolveRef to scope reads and "mark all" / "unread" to the current user. The per-id routes (:id/read, DELETE :id) act on a notification id directly, so guard them so a user can only touch their own rows — pass your auth guard via the guards option (on either controller: { guards } or createNotificationsController({ guards })) so the current user comes from the same req.user your guard populates.

On this page