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:
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.
| Method | Returns | Description |
|---|---|---|
all(target) | StoredNotification[] | Every notification for the target, newest first. |
unread(target) | StoredNotification[] | Unread notifications for the target. |
unreadCount(target) | number | How 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) | void | Mark one notification read, by its id. |
markAllAsRead(target) | void | Mark every notification for the target read. |
delete(id) | void | Remove 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:
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:
| Route | Action |
|---|---|
GET /notifications | Paginated list (?page & ?perPage). |
GET /notifications/unread | Unread notifications. |
GET /notifications/unread/count | { count } of unread. |
POST /notifications/:id/read | Mark one read. |
POST /notifications/read-all | Mark all read for the current user. |
DELETE /notifications/:id | Delete 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.
Real-time & in-app
nestjs-notifications is also your real-time delivery layer. Persist with the database channel, push live with SSE or WebSocket, and consume in React — the whole in-app notification loop, and how to pick SSE vs WebSocket.
React inbox widget
Drop a notification inbox into a React app with one component. The @dudousxd/nestjs-notifications-react package ships <Inbox/>, a NotificationsProvider, and the useNotifications / useUnreadCount hooks — all consuming the read API and SSE stream you already expose.