@flusys/nestjs-notification
v4.1.1
Published
Notification module with real-time Socket.IO support
Downloads
719
Maintainers
Readme
@flusys/nestjs-notification
Real-time notification system for NestJS — WebSocket via Socket.IO, persistent PostgreSQL storage, read/unread tracking, company broadcasting, and a cross-module adapter for dependency-free integration.
Table of Contents
- Overview
- Features
- Compatibility
- Installation
- Quick Start
- Module Registration
- Configuration Reference
- Feature Toggles
- WebSocket Gateway
- API Endpoints
- Entities
- Cross-Module Adapter (NOTIFICATION_ADAPTER)
- Exported Services
- Sending Notifications Programmatically
- Frontend Integration
- Troubleshooting
- License
Overview
@flusys/nestjs-notification delivers notifications both in real-time (Socket.IO WebSocket) and persistently (PostgreSQL). Unread notifications survive disconnections and are delivered on the next connection. The NOTIFICATION_ADAPTER allows auth, email, and other modules to send notifications without importing this package directly.
Features
- Persistent storage — Notifications stored in PostgreSQL with read/unread tracking
- Real-time WebSocket — Socket.IO gateway at
/notificationsnamespace - JWT authentication on WebSocket — Token verified on connection handshake
- Read tracking —
isRead+readAttimestamp per notification - Mark as read — Single notification or bulk "mark all as read"
- Unread count — Fast count query for notification badge
- Company scoping — Filter by company in multi-company setups
- Broadcast to company — Emit to all users in a company Socket.IO room
- Online presence — Check if a user is currently connected
- Cross-module adapter —
NOTIFICATION_ADAPTERfor sending from any module - Configurable realtime — Disable Socket.IO gateway when not needed
Compatibility
| Package | Version |
|---------|---------|
| @flusys/nestjs-core | ^4.0.0 |
| @flusys/nestjs-shared | ^4.0.0 |
| @nestjs/websockets | ^11.0.0 |
| @nestjs/platform-socket.io | ^11.0.0 |
| socket.io | ^4.0.0 |
| jsonwebtoken | ^9.0.0 |
| Node.js | >= 18.x |
Installation
npm install @flusys/nestjs-notification @flusys/nestjs-shared @flusys/nestjs-core
npm install @nestjs/platform-socket.io socket.ioQuick Start
With Realtime Enabled
import { Module } from '@nestjs/common';
import { NotificationModule } from '@flusys/nestjs-notification';
@Module({
imports: [
NotificationModule.forRoot({
global: true,
includeController: true,
bootstrapAppConfig: {
databaseMode: 'single',
enableCompanyFeature: false,
},
config: {
enableRealtime: true,
jwtSecret: process.env.JWT_SECRET,
defaultDatabaseConfig: {
type: 'postgres',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT ?? 5432),
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
},
},
}),
],
})
export class AppModule {}Using NOTIFICATION_ADAPTER in Another Module
// In auth.service.ts — sends a notification after login
import { NOTIFICATION_ADAPTER } from '@flusys/nestjs-shared/interfaces';
import { INotificationAdapter } from '@flusys/nestjs-shared/interfaces';
@Injectable()
export class AuthenticationService {
constructor(
@Optional() @Inject(NOTIFICATION_ADAPTER)
private readonly notificationAdapter?: INotificationAdapter,
) {}
async afterLogin(userId: string): Promise<void> {
await this.notificationAdapter?.send({
userId,
title: 'New Login',
message: 'Your account was accessed from a new device.',
type: 'warning',
});
}
}Module Registration
forRoot (Sync)
NotificationModule.forRoot({
global?: boolean;
includeController?: boolean; // Default: true
bootstrapAppConfig?: {
databaseMode: 'single' | 'multi-tenant';
enableCompanyFeature: boolean;
};
config?: {
enableRealtime?: boolean; // Default: true — registers Socket.IO gateway
jwtSecret?: string; // Required for WebSocket JWT authentication
defaultDatabaseConfig?: IDatabaseConfig;
tenants?: ITenantDatabaseConfig[];
};
})Behavior:
enableRealtime: false→NotificationGatewayis NOT registered (no WebSocket)includeController: false→ No HTTP endpoints registeredglobal: true→ Exports available application-wide
forRootAsync (Factory)
import { ConfigService } from '@nestjs/config';
NotificationModule.forRootAsync({
global: true,
includeController: true,
bootstrapAppConfig: {
databaseMode: 'single',
enableCompanyFeature: true,
},
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
enableRealtime: true,
jwtSecret: configService.get('JWT_SECRET'),
defaultDatabaseConfig: {
type: 'postgres',
host: configService.get('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
},
}),
inject: [ConfigService],
})Exported services:
NotificationConfigServiceNotificationDataSourceProviderNotificationServiceNotificationHelperServiceNotificationGateway(whenenableRealtime: true)NOTIFICATION_ADAPTER
Configuration Reference
interface INotificationModuleConfig extends IDataSourceServiceOptions {
/** Enable Socket.IO WebSocket gateway (default: true) */
enableRealtime?: boolean;
/** JWT secret for WebSocket connection authentication */
jwtSecret?: string;
}Feature Toggles
| Feature | Config | Default | Effect |
|---------|--------|---------|--------|
| WebSocket gateway | enableRealtime: true | true | Registers NotificationGateway on /notifications namespace |
| Company scoping | enableCompanyFeature: true | false | Uses NotificationWithCompany entity; adds company room broadcasting |
| Multi-tenant | databaseMode: 'multi-tenant' | 'single' | Per-tenant DataSource connections |
WebSocket Gateway
The gateway runs on the /notifications Socket.IO namespace. JWT authentication is required on connection.
Client Connection
Browser (JavaScript):
import { io } from 'socket.io-client';
const socket = io('http://localhost:2002/notifications', {
auth: { token: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' },
transports: ['websocket'],
});
socket.on('connect', () => {
console.log('Connected to notification gateway');
});
socket.on('notification', (data) => {
console.log('New notification:', data);
// { id, title, message, type, data, createdAt }
});
socket.on('disconnect', () => {
console.log('Disconnected');
});Angular service:
import { io, Socket } from 'socket.io-client';
@Injectable({ providedIn: 'root' })
export class NotificationSocketService {
private socket: Socket;
connect(token: string): void {
this.socket = io(`${environment.apiUrl}/notifications`, {
auth: { token: `Bearer ${token}` },
});
this.socket.on('notification', (data) => {
this.handleNewNotification(data);
});
}
disconnect(): void {
this.socket?.disconnect();
}
}WebSocket Events
Server → Client (emitted by the gateway):
| Event | Payload | Description |
|-------|---------|-------------|
| notification | INotificationPayload | New notification delivered in real-time |
| notification:count | { count: number } | Updated unread count |
Client → Server (handled by the gateway):
| Event | Payload | Description |
|-------|---------|-------------|
| join:company | { companyId: string } | Join a company room for broadcast notifications |
| leave:company | { companyId: string } | Leave a company room |
| ping | — | Keepalive |
API Endpoints
All endpoints use POST and require JWT authentication.
Notifications — POST /notification/*
| Endpoint | Auth | Description |
|----------|------|-------------|
| POST /notification/get-all | JWT | Get current user's notifications |
| POST /notification/get-unread-count | JWT | Get unread notification count |
| POST /notification/mark-read | JWT | Mark a single notification as read |
| POST /notification/mark-all-read | JWT | Mark all notifications as read |
| POST /notification/delete | JWT | Delete a notification |
| POST /notification/get/:id | JWT | Get a notification by ID |
Admin — POST /notification/admin/*
| Endpoint | Permission | Description |
|----------|-----------|-------------|
| POST /notification/admin/send | notification.create | Send notification to specific user(s) |
| POST /notification/admin/broadcast | notification.create | Broadcast to all users in a company |
| POST /notification/admin/get-all | notification.read | List all notifications (all users) |
Send notification request:
POST /notification/admin/send
{
"userId": "uuid",
"title": "Order Shipped",
"message": "Your order #12345 has been shipped.",
"type": "success",
"data": { "orderId": "12345", "trackingUrl": "https://..." }
}Broadcast to company:
POST /notification/admin/broadcast
{
"companyId": "uuid",
"title": "System Maintenance",
"message": "The system will be down for maintenance at 2:00 AM.",
"type": "warning"
}Entities
Core Entities
| Entity | Table | Description |
|--------|-------|-------------|
| Notification | notification | User notification with type, title, message, JSON data, isRead, readAt |
Indexes: (userId, isRead), (userId, createdAt) for fast unread queries.
Company Feature Entities (enableCompanyFeature: true)
| Entity | Table | Description |
|--------|-------|-------------|
| NotificationWithCompany | notification | Same + companyId column |
Additional index: (companyId, userId, isRead)
import { NotificationModule } from '@flusys/nestjs-notification';
TypeOrmModule.forRoot({
entities: [
...NotificationModule.getEntities({ enableCompanyFeature: true }),
],
})Cross-Module Adapter (NOTIFICATION_ADAPTER)
The NOTIFICATION_ADAPTER token is defined in @flusys/nestjs-shared so any module can send notifications without importing @flusys/nestjs-notification.
When NotificationModule is registered in the app, it provides the implementation. When it is not registered, @Optional() prevents injection errors.
INotificationAdapter interface (from @flusys/nestjs-shared):
interface INotificationAdapter {
send(data: INotificationSendData): Promise<void>;
sendBulk(data: INotificationSendData[]): Promise<void>;
broadcast(companyId: string, data: Omit<INotificationSendData, 'userId'>): Promise<void>;
isUserOnline(userId: string): boolean;
}
interface INotificationSendData {
userId: string;
title: string;
message: string;
type?: 'info' | 'success' | 'warning' | 'error';
data?: Record<string, any>;
companyId?: string;
}Usage pattern:
import { NOTIFICATION_ADAPTER, INotificationAdapter } from '@flusys/nestjs-shared/interfaces';
@Injectable()
export class OrderService {
constructor(
@Optional() @Inject(NOTIFICATION_ADAPTER)
private readonly notifications?: INotificationAdapter,
) {}
async shipOrder(orderId: string, userId: string): Promise<void> {
// ... ship order logic
// Send notification if adapter is available
await this.notifications?.send({
userId,
title: 'Order Shipped',
message: `Your order has been shipped.`,
type: 'success',
data: { orderId },
});
}
}Exported Services
| Service | Description |
|---------|-------------|
| NotificationService | Notification CRUD (REQUEST-scoped) |
| NotificationHelperService | Singleton service for programmatic sending |
| NotificationGateway | Socket.IO gateway (when enableRealtime: true) |
| NotificationConfigService | Runtime config and feature flags |
| NotificationDataSourceProvider | Dynamic DataSource per request |
| NOTIFICATION_ADAPTER | Cross-module adapter implementation |
Sending Notifications Programmatically
Use NotificationHelperService (singleton) within the same application:
import { NotificationHelperService } from '@flusys/nestjs-notification';
@Injectable()
export class PaymentService {
constructor(
@Inject(NotificationHelperService)
private readonly notificationHelper: NotificationHelperService,
) {}
async afterPayment(userId: string, amount: number): Promise<void> {
await this.notificationHelper.send({
userId,
title: 'Payment Received',
message: `We received your payment of $${amount}.`,
type: 'success',
data: { amount },
});
}
async notifyCompany(companyId: string): Promise<void> {
await this.notificationHelper.broadcast(companyId, {
title: 'Company Announcement',
message: 'Check your email for important updates.',
type: 'info',
});
}
}Frontend Integration
Unread Badge Count
// Poll for unread count (or update via WebSocket event 'notification:count')
async getUnreadCount(): Promise<number> {
const response = await this.http.post('/notification/get-unread-count', {}).toPromise();
return response.data.count;
}Notification List
async getNotifications(page = 1, pageSize = 20) {
return this.http.post('/notification/get-all', { page, pageSize }).toPromise();
}Mark as Read
// Single
await this.http.post('/notification/mark-read', { id: notificationId }).toPromise();
// All
await this.http.post('/notification/mark-all-read', {}).toPromise();Troubleshooting
WebSocket connection rejected (401)
The JWT token in the auth.token field is invalid or expired. Ensure you pass Bearer <token> format:
auth: { token: `Bearer ${accessToken}` }Notifications delivered via REST but not real-time
Check that enableRealtime: true is set in the config and @nestjs/platform-socket.io is installed. Also verify that the client is connecting to the correct namespace (/notifications).
Company broadcast not reaching all users
Users must join the company room via the join:company WebSocket event after connecting. If enableCompanyFeature: false, company rooms are not created.
NOTIFICATION_ADAPTER is undefined
NotificationModule is not registered. Use @Optional() @Inject(NOTIFICATION_ADAPTER) so the injection doesn't throw when the module is absent.
No metadata for entity
Register entities in TypeOrmModule:
entities: [...NotificationModule.getEntities({ enableCompanyFeature: true })]License
MIT © FLUSYS
Part of the FLUSYS framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.
