
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
A comprehensive step-by-step guide to implement a multi-channel notification service using NotiCore in a NestJS application.
NotiCore is a powerful notification management library that provides:
┌─────────────────────────────────────────────────────────────────┐
│ Your Application │
└───────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ NotificationService │
│ - Creates notification records │
│ - Manages notification lifecycle │
│ - Publishes events to queue │
└───────────────────────────┬─────────────────────────────────────┘
│
▼
┌───────────────┐
│ Message Queue │
│ (Kafka/etc) │
└───────┬───────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ NotificationDeliveryService │
│ - Consumes queue events │
│ - Prepares notification templates │
│ - Delivers via channels (SMS/Email/Push) │
│ - Handles retries and failures │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ FCM │ │ SMS.to │ │Mailgun │
│ (Push) │ │ (SMS) │ │ (Email) │
└─────────┘ └─────────┘ └─────────┘
Before starting, ensure you have:
npm install noticore
npm install firebase-admin
npm install @nestjs/typeorm typeorm pg
npm install kafka-nestjs # or your message queue library
Create src/config/communication.config.ts:
import { registerAs } from '@nestjs/config';
import * as admin from 'firebase-admin';
import {
NotiCoreFCMConfigType,
NotiCoreMailgunConfigType,
NotiCoreSmsToConfigType
} from 'noticore';
import { IsNotEmpty, IsString } from 'class-validator';
export const CommunicationConfigToken = 'COMMUNICATION';
export type CommunicationConfig = {
smsToConfig: NotiCoreSmsToConfigType;
fcmConfig: NotiCoreFCMConfigType;
emailConfig: NotiCoreMailgunConfigType;
};
class CommunicationEnvSchema {
@IsString()
@IsNotEmpty()
MAILGUN_DOMAIN!: string;
@IsString()
@IsNotEmpty()
MAILGUN_FROM!: string;
@IsString()
@IsNotEmpty()
MAILGUN_API_KEY!: string;
@IsString()
@IsNotEmpty()
SMSTO_URL!: string;
@IsString()
@IsNotEmpty()
SMSTO_PROVIDER_TOKEN!: string;
@IsString()
@IsNotEmpty()
SMSTO_NUMBER!: string;
}
export function getCommunicationConfig(): CommunicationConfig {
const ENV = process.env;
return {
smsToConfig: {
providerToken: ENV.SMSTO_PROVIDER_TOKEN!,
url: ENV.SMSTO_URL!,
number: ENV.SMSTO_NUMBER!,
},
fcmConfig: {
serviceAccount: require('../../firebase-service-key.json') as admin.ServiceAccount,
},
emailConfig: {
domain: ENV.MAILGUN_DOMAIN!,
from: ENV.MAILGUN_FROM!,
key: ENV.MAILGUN_API_KEY!,
ssl: false,
logging: false,
},
};
}
export const communicationConfig = registerAs(
CommunicationConfigToken,
(): CommunicationConfig => getCommunicationConfig()
);
In your src/main.ts:
import { NotiCoreNotificationConfigService } from 'noticore';
import { getCommunicationConfig } from './config/communication.config';
async function bootstrap(): Promise<void> {
// Initialize NotiCore configurations BEFORE creating the app
const communicationConfig = getCommunicationConfig();
NotiCoreNotificationConfigService.initializeFCMConfig(
communicationConfig.fcmConfig
);
NotiCoreNotificationConfigService.initializeSmsToConfig(
communicationConfig.smsToConfig
);
NotiCoreNotificationConfigService.initializeEmailConfig(
communicationConfig.emailConfig
);
const app = await NestFactory.create(AppModule);
// ... rest of your bootstrap code
}
Add to your .env file:
# Firebase Push Notifications
# (Service account JSON file path configured in code)
# SMS Configuration
SMSTO_URL=https://api.sms.to
SMSTO_PROVIDER_TOKEN=your_smsto_token
SMSTO_NUMBER=+1234567890
# Email Configuration
MAILGUN_DOMAIN=mg.yourdomain.com
MAILGUN_FROM=noreply@yourdomain.com
MAILGUN_API_KEY=key-xxxxxxxxxxxxx
Create src/notification/entities/notification.entity.ts:
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { BaseEntity } from 'src/common/entities/base.entity';
import {
INotiCoreNotificationEntity,
NotiCoreNotificationStatusEnum
} from 'noticore';
@Entity({ name: 'notification' })
@Index('idx_notification_user_id', ['userId'])
@Index('idx_notification_status', ['status'])
@Index('idx_notification_retry_count', ['retryCount'])
export class NotificationEntity
extends BaseEntity
implements INotiCoreNotificationEntity
{
@Column({ type: 'enum', enum: NotificationTypeEnum })
type: NotificationTypeEnum;
@Column({ type: 'uuid' })
referenceId: string;
@Column({ type: 'enum', enum: NotificationReferenceTypeEnum })
referenceType: NotificationReferenceTypeEnum;
@Column({ type: 'uuid' })
userId: string;
@Column({
type: 'enum',
enum: NotiCoreNotificationStatusEnum,
default: NotiCoreNotificationStatusEnum.CREATED,
})
status: NotiCoreNotificationStatusEnum;
@Column({ type: 'varchar', default: null })
errorMessage: string | null;
@Column({ default: 0 })
retryCount: number;
@Column({ type: 'timestamp', default: null })
retryAt: Date | null;
@Column({ type: 'timestamp', default: null })
seenAt: Date | null;
@Column({ type: 'timestamp', nullable: true })
expireAt: Date | null;
}
Create src/notification/entities/notification-delivery.entity.ts:
import { Column, Entity, Index } from 'typeorm';
import { BaseEntity } from 'src/common/entities/base.entity';
import {
INotiCoreDeliveryEntity,
INotiCoreNotificationData,
INotiCoreNotificationPayload,
NotiCoreDeliveryChannelEnum,
NotiCoreNotificationStatusEnum,
} from 'noticore';
export class NotificationPayload implements INotiCoreNotificationPayload {
body: string;
title: string;
imageUrl?: string;
avatarUrl?: string;
}
export class NotificationData implements INotiCoreNotificationData {
type: string;
referenceId: string;
referenceType: string;
}
@Entity({ name: 'notification_delivery' })
@Index('idx_notification_delivery_channelType', ['channelType'])
@Index('idx_notification_delivery_user_id', ['userId'])
@Index('idx_notification_delivery_status', ['status'])
export class NotificationDeliveryEntity
extends BaseEntity
implements INotiCoreDeliveryEntity
{
@Column({ type: 'jsonb', nullable: false })
payload: NotificationPayload;
@Column({ type: 'jsonb', nullable: false })
data: NotificationData;
@Column({ type: 'uuid' })
userId: string;
@Column({
type: 'enum',
enum: NotiCoreDeliveryChannelEnum,
nullable: false,
})
channelType: NotiCoreDeliveryChannelEnum;
@Column({
type: 'enum',
enum: NotiCoreNotificationStatusEnum,
default: NotiCoreNotificationStatusEnum.CREATED,
})
status: NotiCoreNotificationStatusEnum;
@Column({ type: 'varchar', nullable: false })
recipient: string;
@Column({ type: 'varchar', default: null })
errorMessage: string | null;
@Column({ default: 0 })
retryCount: number;
}
Create src/notification/queues/notification.event-publisher.ts:
import { Injectable } from '@nestjs/common';
import { KafkaProducerService } from 'kafka-nestjs';
import {
INotiCoreDeliveryMQEventPublisher,
INotiCoreNotificationMQEventPublisher
} from 'noticore';
@Injectable()
export class NotificationEventPublisher
implements
INotiCoreNotificationMQEventPublisher,
INotiCoreDeliveryMQEventPublisher
{
constructor(
private readonly kafkaProducerService: KafkaProducerService
) {}
async publishNotificationCreated(dto: any[]): Promise<void> {
await this.kafkaProducerService.send({
topic: 'notification.created',
messages: dto.map((message) => ({
value: JSON.stringify(message),
})),
});
}
async publishNotificationScheduled(dto: any[]): Promise<void> {
await this.kafkaProducerService.send({
topic: 'notification.scheduled',
messages: dto.map((message) => ({
value: JSON.stringify(message),
})),
});
}
}
Create src/notification/services/notification.service.ts:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
NotiCoreNotificationRepositoryFactory,
NotiCoreNotificationService,
NotiCoreServiceFactory,
RepositoryOptionEnum,
} from 'noticore';
import { NotificationEntity } from '../entities/notification.entity';
import { NotificationEventPublisher } from '../queues/notification.event-publisher';
@Injectable()
export class NotificationService {
readonly ncClient: NotiCoreNotificationService<NotificationEntity>;
constructor(
private readonly notificationEventPublisher: NotificationEventPublisher,
@InjectRepository(NotificationEntity)
private notificationRepository: Repository<NotificationEntity>,
) {
// Initialize NotiCore Notification Service
this.ncClient = NotiCoreServiceFactory.createNotificationService({
eventPublisher: this.notificationEventPublisher,
repository: NotiCoreNotificationRepositoryFactory.createNotificationRepository(
RepositoryOptionEnum.TYPEORM,
this.notificationRepository,
),
});
}
// Example: Create notifications
async createNotifications(dto: {
userIds: string[];
type: string;
referenceId: string;
referenceType: string;
}): Promise<void> {
await this.ncClient.bulkInsert({
data: dto.userIds.map(userId => ({
userId,
type: dto.type,
referenceId: dto.referenceId,
referenceType: dto.referenceType,
status: NotiCoreNotificationStatusEnum.CREATED,
retryCount: 0,
})),
});
}
}
Create src/notification/services/notification-delivery.service.ts:
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
NotiCoreDeliveryService,
NotiCoreMessageSenderFactory,
NotiCoreNotificationRepositoryFactory,
NotiCoreServiceFactory,
NotiCoreDeliveryChannelEnum,
RepositoryOptionEnum,
} from 'noticore';
import { NotificationDeliveryEntity } from '../entities/notification-delivery.entity';
import { NotificationEventPublisher } from '../queues/notification.event-publisher';
@Injectable()
export class NotificationDeliveryService {
private readonly ncDelivery: NotiCoreDeliveryService<NotificationDeliveryEntity>;
constructor(
private readonly eventPublisher: NotificationEventPublisher,
@InjectRepository(NotificationDeliveryEntity)
private notificationDeliveryRepository: Repository<NotificationDeliveryEntity>,
) {
// Create message senders for each channel
const smsSender = NotiCoreMessageSenderFactory.createSender(
NotiCoreDeliveryChannelEnum.SMS
);
const fcmSender = NotiCoreMessageSenderFactory.createSender(
NotiCoreDeliveryChannelEnum.PUSH
);
const emailSender = NotiCoreMessageSenderFactory.createSender(
NotiCoreDeliveryChannelEnum.EMAIL
);
// Initialize NotiCore Delivery Service
this.ncDelivery = NotiCoreServiceFactory.createDeliveryService({
repository: NotiCoreNotificationRepositoryFactory.createNotificationDeliveryRepository(
RepositoryOptionEnum.TYPEORM,
this.notificationDeliveryRepository,
),
eventPublisher: this.eventPublisher,
smsSender,
fcmSender,
emailSender,
});
}
// Example: Handle notification created event
async handleNotificationCreated(dto: {
userId: string;
channelType: NotiCoreDeliveryChannelEnum;
payload: { title: string; body: string };
data: any;
recipients: Set<string>;
}): Promise<void> {
await this.ncDelivery.handleNotificationCreated({
channelType: dto.channelType,
userId: dto.userId,
payload: dto.payload,
priority: NotiCorePriorityEnum.HIGH,
data: dto.data,
recipients: dto.recipients,
handleInvalidFCMTokens: async (invalidTokens: string[]) => {
// Clean up invalid FCM tokens
console.log('Invalid tokens:', invalidTokens);
},
});
}
}
Create src/notification/notification.module.ts:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NotificationDeliveryService } from './services/notification-delivery.service';
import { NotificationService } from './services/notification.service';
import { NotificationDeliveryEntity } from './entities/notification-delivery.entity';
import { NotificationEntity } from './entities/notification.entity';
import { NotificationEventPublisher } from './queues/notification.event-publisher';
@Module({
imports: [
TypeOrmModule.forFeature([
NotificationDeliveryEntity,
NotificationEntity
]),
],
providers: [
NotificationDeliveryService,
NotificationService,
NotificationEventPublisher,
],
exports: [NotificationService],
})
export class NotificationModule {}
Add the module to your AppModule:
import { NotificationModule } from 'src/notification/notification.module';
@Module({
imports: [
// ... other modules
NotificationModule,
],
})
export class AppModule {}
import { Injectable } from '@nestjs/common';
import { NotificationDeliveryService } from './services/notification-delivery.service';
import { NotiCoreDeliveryChannelEnum } from 'noticore';
@Injectable()
export class AnnouncementService {
constructor(
private readonly notificationDeliveryService: NotificationDeliveryService,
) {}
async sendAnnouncement(userId: string, fcmToken: string): Promise<void> {
await this.notificationDeliveryService.handleNotificationCreated({
userId,
channelType: NotiCoreDeliveryChannelEnum.PUSH,
payload: {
title: 'New Announcement',
body: 'Check out our latest updates!',
},
data: {
type: 'announcement',
referenceId: '123e4567-e89b-12d3-a456-426614174000',
referenceType: 'announcement',
},
recipients: new Set([fcmToken]),
});
}
}
async sendEmailNotification(userId: string, email: string): Promise<void> {
await this.notificationDeliveryService.handleNotificationCreated({
userId,
channelType: NotiCoreDeliveryChannelEnum.EMAIL,
payload: {
title: 'Welcome to Our Platform',
body: '<h1>Welcome!</h1><p>Thank you for joining us.</p>',
},
data: {
type: 'welcome',
referenceId: userId,
referenceType: 'user',
},
recipients: new Set([email]),
});
}
async sendSMSNotification(userId: string, phoneNumber: string): Promise<void> {
await this.notificationDeliveryService.handleNotificationCreated({
userId,
channelType: NotiCoreDeliveryChannelEnum.SMS,
payload: {
title: 'Verification Code',
body: 'Your verification code is: 123456',
},
data: {
type: 'verification',
referenceId: userId,
referenceType: 'user',
},
recipients: new Set([phoneNumber]),
});
}
async sendMultiChannelNotification(
userId: string,
email: string,
phoneNumber: string,
fcmToken: string,
): Promise<void> {
const channels = [
{
channelType: NotiCoreDeliveryChannelEnum.EMAIL,
recipients: new Set([email]),
},
{
channelType: NotiCoreDeliveryChannelEnum.SMS,
recipients: new Set([phoneNumber]),
},
{
channelType: NotiCoreDeliveryChannelEnum.PUSH,
recipients: new Set([fcmToken]),
},
];
const promises = channels.map((channel) =>
this.notificationDeliveryService.handleNotificationCreated({
userId,
channelType: channel.channelType,
payload: {
title: 'Important Update',
body: 'Please check your account for important updates.',
},
data: {
type: 'update',
referenceId: userId,
referenceType: 'user',
},
recipients: channel.recipients,
}),
);
await Promise.all(promises);
}
Create test/shared/helpers/notification/mocks.ts:
import {
INotiCoreEmailMessage,
INotiCorePushMessage,
MailgunEdgeService,
SmsToEdgeService,
FCMEdgeService,
} from 'noticore';
export function mockSMSService() {
const sendEach = jest
.spyOn(SmsToEdgeService.prototype, 'sendEach')
.mockImplementation(async () => {
return Promise.resolve({ success: true });
});
const send = jest
.spyOn(SmsToEdgeService.prototype, 'send')
.mockImplementation(async () => {
return Promise.resolve({ success: true });
});
return { sendEach, send };
}
export function mockFCMEdgeService() {
const sendEach = jest
.spyOn(FCMEdgeService.prototype, 'sendEach')
.mockImplementation((messages: INotiCorePushMessage[]) => {
return Promise.resolve(
messages.map((message) => ({
success: true,
id: message.id,
token: message.recipient,
})),
);
});
const send = jest
.spyOn(FCMEdgeService.prototype, 'send')
.mockImplementation((message: INotiCorePushMessage) => {
return Promise.resolve({
success: true,
id: message.id,
token: message.recipient,
});
});
return { sendEach, send };
}
export function mockEmailEdgeService() {
const sendEach = jest
.spyOn(MailgunEdgeService.prototype, 'sendEach')
.mockImplementation((messages: INotiCoreEmailMessage[]) => {
return Promise.resolve(
messages.map((message) => ({
success: true,
data: {
id: message.id,
message: message.payload.body,
status: 200,
},
})),
);
});
const send = jest
.spyOn(MailgunEdgeService.prototype, 'send')
.mockImplementation((message: INotiCoreEmailMessage) => {
return Promise.resolve({
success: true,
data: {
id: message.id,
message: message.payload.body,
status: 200,
},
});
});
return { sendEach, send };
}
import { Test, TestingModule } from '@nestjs/testing';
import { NotificationDeliveryService } from './notification-delivery.service';
import { mockFCMEdgeService } from '../../test/shared/helpers/notification/mocks';
describe('NotificationDeliveryService', () => {
let service: NotificationDeliveryService;
let fcmMock: ReturnType<typeof mockFCMEdgeService>;
beforeEach(async () => {
fcmMock = mockFCMEdgeService();
const module: TestingModule = await Test.createTestingModule({
providers: [NotificationDeliveryService],
}).compile();
service = module.get<NotificationDeliveryService>(
NotificationDeliveryService,
);
});
it('should send push notification', async () => {
await service.handleNotificationCreated({
userId: 'user-123',
channelType: NotiCoreDeliveryChannelEnum.PUSH,
payload: {
title: 'Test',
body: 'Test notification',
},
data: { type: 'test' },
recipients: new Set(['token-123']),
});
expect(fcmMock.send).toHaveBeenCalled();
});
});
// ✅ Good
async function bootstrap() {
const config = getCommunicationConfig();
NotiCoreNotificationConfigService.initializeFCMConfig(config.fcmConfig);
const app = await NestFactory.create(AppModule);
// ...
}
// ❌ Bad - Config initialized after app creation
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = getCommunicationConfig();
NotiCoreNotificationConfigService.initializeFCMConfig(config.fcmConfig);
}
Create reusable template factories:
export function notificationTemplateFactory({
referenceType,
channelType,
data,
}: {
referenceType: string;
channelType: NotiCoreDeliveryChannelEnum;
data: any;
}): string {
switch (referenceType) {
case 'announcement':
return channelType === NotiCoreDeliveryChannelEnum.EMAIL
? `<html><body>${data.template}</body></html>`
: data.template;
default:
return data.template;
}
}
Always provide a callback to clean up invalid tokens:
await this.ncDelivery.handleNotificationCreated({
// ... other params
handleInvalidFCMTokens: async (invalidTokens: string[]) => {
// Remove invalid tokens from your database
await this.sessionService.removeFcmTokens(invalidTokens);
},
});
Add proper indexes to your entities for better performance:
@Index('idx_notification_user_id_status', ['userId', 'status'])
@Index('idx_notification_retry_at', ['retryAt'])
NotiCore handles retries automatically, but you can customize:
// In your cron service
@Cron('*/5 * * * *') // Every 5 minutes
async retryFailedNotifications() {
await this.notificationDeliveryService.deliverPendingNotifications({
limit: 100,
maxRetries: 3,
});
}
export enum NotificationTypeEnum {
ANNOUNCEMENT = 'announcement',
WELCOME = 'welcome',
VERIFICATION = 'verification',
}
export enum NotificationReferenceTypeEnum {
ANNOUNCEMENT = 'announcement',
USER = 'user',
}
Query notification status for debugging:
async getNotificationStatus(notificationId: string) {
return await this.notificationRepository.findOne({
where: { id: notificationId },
select: ['id', 'status', 'errorMessage', 'retryCount'],
});
}
Wrap notification calls in try-catch:
try {
await this.notificationDeliveryService.handleNotificationCreated({
// ... params
});
} catch (error) {
this.logger.error('Failed to send notification', error);
// Don't throw - let the retry mechanism handle it
}
Problem: Push notifications not being delivered.
Solution:
Problem: Email or SMS not being delivered.
Solution:
Problem: Notifications not progressing to SENT status.
Solution:
You've now learned how to:
✅ Install and configure NotiCore
✅ Set up database entities for notifications
✅ Create notification and delivery services
✅ Implement event publishers
✅ Send multi-channel notifications (SMS, Email, Push)
✅ Test notification services
✅ Follow best practices for production use
Start sending notifications with confidence! 🚀
Behrad Kazemi
FAQs
a flexible library for making scaleable notification system
We found that noticore demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.