Multi-channel notification system for NestJS — send notifications via database, email, and custom channels with a fluent API.
This package provides a notification system for NestJS that sends notifications through multiple channels (database, email, custom) using a clean class-based API with TypeORM integration.
Once installed, using it is as simple as:
await notificationService.send(user, new WelcomeNotification(user));
// Or via the entity mixin
await user.notify(new WelcomeNotification(user));
// Query notifications
const unread = await user.unreadNotifications();
const count = await user.getUnreadNotificationCount();- Installation
- Quick Start
- Module Configuration
- Creating Notifications
- Sending Notifications
- Database Channel
- Mail Channel
- Custom Channels
- Entity Decorator & Mixin
- Querying Notifications
- Read/Unread Tracking
- Events
- Configuration Options
- Testing
- Changelog
- Contributing
- Security
- License
Install the package via npm:
npm install @nestbolt/notificationsOr via yarn:
yarn add @nestbolt/notificationsOr via pnpm:
pnpm add @nestbolt/notificationsThis package requires the following peer dependencies, which you likely already have in a NestJS project:
@nestjs/common ^10.0.0 || ^11.0.0
@nestjs/core ^10.0.0 || ^11.0.0
@nestjs/typeorm ^10.0.0 || ^11.0.0
typeorm ^0.3.0
reflect-metadata ^0.1.13 || ^0.2.0
Optional peer dependencies:
nodemailer ^6.0.0 (for the mail channel)
@nestjs/event-emitter ^2.0.0 || ^3.0.0 (for lifecycle events)
import { NotificationModule } from "@nestbolt/notifications";
@Module({
imports: [
NotificationModule.forRoot({
channels: {
database: true,
},
}),
],
})
export class AppModule {}import { Notification, MailMessage } from "@nestbolt/notifications";
class WelcomeNotification extends Notification {
constructor(private user: { name: string }) {
super();
}
via(): string[] {
return ["database"];
}
toDatabase() {
return { message: `Welcome ${this.user.name}!`, type: "welcome" };
}
}import { NotificationService } from "@nestbolt/notifications";
@Injectable()
export class UserService {
constructor(private readonly notifications: NotificationService) {}
async onboardUser(user: NotifiableEntity): Promise<void> {
await this.notifications.send(user, new WelcomeNotification(user));
}
}NotificationModule.forRoot({
channels: {
database: true,
mail: {
transport: {
host: "smtp.example.com",
port: 587,
auth: { user: "user", pass: "pass" },
},
defaults: { from: "noreply@example.com" },
},
},
});NotificationModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
channels: {
database: true,
mail: {
transport: {
host: config.get("SMTP_HOST"),
port: config.get("SMTP_PORT"),
auth: {
user: config.get("SMTP_USER"),
pass: config.get("SMTP_PASS"),
},
},
defaults: { from: config.get("MAIL_FROM") },
},
},
}),
});The module is registered as global — you don't need to import it in every module.
Each notification is a class extending Notification:
import { Notification, MailMessage } from "@nestbolt/notifications";
class OrderShippedNotification extends Notification {
constructor(
private order: { id: string; trackingUrl: string },
) {
super();
}
via(): string[] {
return ["database", "mail"];
}
toDatabase() {
return {
message: `Your order #${this.order.id} has shipped!`,
orderId: this.order.id,
};
}
toMail() {
return new MailMessage()
.subject("Your order has shipped!")
.greeting("Hello")
.line(`Your order #${this.order.id} is on its way.`)
.action("Track Order", this.order.trackingUrl)
.salutation("Thank you for your purchase");
}
}await notificationService.send(user, new OrderShippedNotification(order));await notificationService.sendToMany(users, new OrderShippedNotification(order));The database channel stores notifications in a notifications table. It's enabled by default.
The NotificationEntity has the following columns:
| Column | Type | Description |
|---|---|---|
id |
uuid |
Primary key |
type |
varchar |
Notification class name |
notifiable_type |
varchar |
Entity type (e.g., "User") |
notifiable_id |
varchar |
Entity ID |
channel |
varchar |
Channel name |
data |
simple-json |
Notification payload |
read_at |
datetime |
When marked as read (nullable) |
created_at |
datetime |
Created timestamp |
updated_at |
datetime |
Updated timestamp |
The mail channel sends emails via nodemailer. Install it as a peer dependency:
pnpm add nodemailerThe recipient email is resolved in order:
notifiable.routeNotificationFor("mail")— if the method exists and returns a valuenotifiable.email— fallback
Build email content with the fluent MailMessage API:
new MailMessage()
.subject("Welcome!")
.from("custom@example.com")
.replyTo("support@example.com")
.cc("manager@example.com")
.cc(["team1@example.com", "team2@example.com"])
.bcc("archive@example.com")
.greeting("Hello John")
.line("Thanks for joining our platform.")
.line("Here are some next steps.")
.action("Get Started", "https://example.com/dashboard")
.salutation("Best regards")
.attach({ filename: "guide.pdf", path: "/path/to/guide.pdf" });The builder generates both HTML and plain text versions automatically.
Register custom channels to send notifications via any transport:
import { Injectable } from "@nestjs/common";
import type { NotificationChannel, NotifiableEntity, Notification } from "@nestbolt/notifications";
@Injectable()
class SlackChannel implements NotificationChannel {
async send(notifiable: NotifiableEntity, notification: Notification): Promise<void> {
const data = (notification as any).toSlack();
// Send to Slack...
}
}
// Register in module (static)
NotificationModule.forRoot({
channels: {
database: true,
custom: { slack: SlackChannel },
},
});
// Or with async configuration
NotificationModule.forRootAsync({
useFactory: () => ({
channels: { database: true },
}),
customChannels: { slack: SlackChannel },
});Then use it in your notification:
class AlertNotification extends Notification {
via(): string[] {
return ["database", "slack"];
}
toSlack() {
return { text: "Alert!", channel: "#alerts" };
}
}Add notification methods directly to your entities:
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import { Notifiable, NotifiableMixin } from "@nestbolt/notifications";
@Entity("users")
@Notifiable()
export class User extends NotifiableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column()
email!: string;
routeNotificationFor(channel: string): string | undefined {
if (channel === "mail") return this.email;
return undefined;
}
}Then use directly on entity instances:
// Send a notification
await user.notify(new WelcomeNotification(user));
// Query notifications
const all = await user.getNotifications();
const unread = await user.unreadNotifications();
const count = await user.getUnreadNotificationCount();
// Mark as read
await user.markNotificationAsRead(notification.id);
await user.markAllNotificationsAsRead();The @Notifiable() decorator accepts an optional notifiableType override:
@Notifiable({ notifiableType: "AppUser" })// All notifications for an entity
const all = await notificationService.getNotifications("User", userId);
// Unread only
const unread = await notificationService.getUnreadNotifications("User", userId);
// Unread count
const count = await notificationService.getUnreadCount("User", userId);const all = await user.getNotifications();
const unread = await user.unreadNotifications();
const count = await user.getUnreadNotificationCount();// Mark a single notification as read
await notificationService.markAsRead(notificationId);
// Mark a notification as unread
await notificationService.markAsUnread(notificationId);
// Mark all notifications as read for an entity
await notificationService.markAllAsRead("User", userId);
// Delete a notification
await notificationService.deleteNotification(notificationId);
// Via the mixin
await user.markNotificationAsRead(notificationId);
await user.markNotificationAsUnread(notificationId);
await user.markAllNotificationsAsRead();When @nestjs/event-emitter is installed, the following events are emitted:
| Event | Payload |
|---|---|
notification.sending |
{ notifiable, notification, channel } |
notification.sent |
{ notifiable, notification, channel } |
notification.failed |
{ notifiable, notification, channel, error } |
notification.read |
{ notification } |
notification.all-read |
{ notifiableType, notifiableId } |
import { OnEvent } from "@nestjs/event-emitter";
import { NOTIFICATION_EVENTS, NotificationSentEvent } from "@nestbolt/notifications";
@Injectable()
export class NotificationListener {
@OnEvent(NOTIFICATION_EVENTS.SENT)
handleNotificationSent(event: NotificationSentEvent) {
console.log(`Notification sent to ${event.notifiable.getNotifiableId()} via ${event.channel}`);
}
}| Option | Type | Default | Description |
|---|---|---|---|
channels.database |
boolean |
true |
Enable database channel |
channels.mail |
MailChannelOptions |
— | Mail channel configuration |
channels.custom |
Record<string, Type<NotificationChannel>> |
— | Custom channel classes |
| Option | Type | Description |
|---|---|---|
transport.host |
string |
SMTP host |
transport.port |
number |
SMTP port |
transport.secure |
boolean |
Use TLS |
transport.auth |
object |
{ user, pass } |
defaults.from |
string |
Default sender address |
| Method | Returns | Description |
|---|---|---|
send(notifiable, notification) |
Promise<void> |
Send to one recipient |
sendToMany(notifiables, notification) |
Promise<void> |
Send to multiple recipients |
getNotifications(type, id) |
Promise<NotificationEntity[]> |
Get all notifications |
getUnreadNotifications(type, id) |
Promise<NotificationEntity[]> |
Get unread notifications |
getUnreadCount(type, id) |
Promise<number> |
Get unread count |
markAsRead(id) |
Promise<void> |
Mark as read |
markAsUnread(id) |
Promise<void> |
Mark as unread |
markAllAsRead(type, id) |
Promise<void> |
Mark all as read |
deleteNotification(id) |
Promise<void> |
Delete a notification |
pnpm testRun tests in watch mode:
pnpm test:watchGenerate coverage report:
pnpm test:covPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.
The MIT License (MIT). Please see License File for more information.