npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nathapp/nestjs-notify

v1.1.0

Published

Notification management with templates, preferences and delivery strategies for NestJS

Downloads

172

Readme

@nathapp/nestjs-notify

Notification management for NestJS: templates, per-user/per-tenant preferences, delivery channels, and delivery tracking. It orchestrates when and how a notification is sent; the actual transport (email/SMS/push) is provided by a delivery channel — either one you register, or ConsoleDeliveryChannel for development.

  • Templates — register and render content with a pluggable ITemplateEngine (an HTML-escaping SimpleTemplateEngine ships by default).
  • Preferences — per (userId, tenantId, channel) opt-in/out, with a metadata bag for richer state (e.g. consent).
  • Delivery channels — pluggable adapters keyed by a channel string. The package ships no transport of its own except a dev-only console logger.
  • Delivery tracking — IDeliveryTracker records normalized delivery events you feed it from provider webhooks.

This package is ORM-agnostic: the core defines repository interfaces, and the @nathapp/nestjs-notify-prisma package supplies Prisma implementations.

Built-in channels

NotificationChannel enumerates four channels. Note the lowercase string values:

| Member | Value | Delivery adapter shipped? | | --- | --- | --- | | NotificationChannel.EMAIL | 'email' | No transport in this package | | NotificationChannel.PUSH | 'push' | No transport in this package | | NotificationChannel.SMS | 'sms' | No transport in this package | | NotificationChannel.IN_APP | 'in_app' | No — in-app delivery is entirely the consumer's concern |

IN_APP ships no delivery adapter. If you send on IN_APP you must register your own channel for it (or accept the CHANNEL_NOT_REGISTERED error).

ConsoleDeliveryChannel is a development/testing adapter that logs the channel name and template code to stdout instead of sending. It reads no recipient or rendered content. It defaults to EMAIL (new ConsoleDeliveryChannel() registers as 'email'), but the constructor accepts any channel name, e.g. new ConsoleDeliveryChannel('sms'). Do not register it in production.

Registering a custom channel

A channel is any @Injectable() class implementing IDeliveryChannel — a channel name plus a send() method. Register it under deliveryChannels, passing an IDeliveryChannelProvider ({ channel, provider }) so Nest can instantiate it with full DI:

import {
  IDeliveryChannel,
  NotificationChannelLike,
  NotifyModule,
  SendNotificationPayload,
} from '@nathapp/nestjs-notify';
import { Injectable, Module } from '@nestjs/common';

@Injectable()
export class WechatMpDeliveryChannel implements IDeliveryChannel {
  readonly channel: NotificationChannelLike = 'wechat_mp';

  async send(
    payload: SendNotificationPayload,
    renderedContent: string,
    renderedSubject?: string,
  ): Promise<void> {
    // call the WeChat subscribeMessage API
  }
}

@Module({
  imports: [
    NotifyModule.register({
      deliveryChannels: [{ channel: 'wechat_mp', provider: WechatMpDeliveryChannel }],
    }),
  ],
})
export class AppModule {}

channel on the instance and channel in the provider entry must be the same string; the module uses the provider entry to build the DI injection token and the instance's own channel as the registry key.

Case sensitivity

Channel keys are case- and separator-sensitive, end to end. The DI token key (getDeliveryChannelToken) and the DeliveryChannelRegistry Map key are both the raw channel string, byte for byte. Therefore:

  • 'EMAIL' and 'email' are two different channels with two different DI tokens and two different registry entries.
  • 'wechat-mp' and 'wechat_mp' are two different channels (as they always were — the separator is never normalized).

History: getDeliveryChannelToken previously upper-cased the channel when building its cache key, which folded case variants onto a single token ('EMAIL' and 'email' both became DELIVERY_CHANNEL_EMAIL). That case folding has been fixed to raw-string keying. Separator variants were never collapsed. Prefer the NotificationChannel enum members or a single lower-case convention in your own code.

The typo hazard

Because NotificationChannelLike is NotificationChannel | (string & Record<never, never>), a mistyped channel name is no longer a compile error — the widened type deliberately accepts arbitrary strings. Two failure modes follow:

  1. At send time: sending on an unregistered channel throws NotifyException(NotifyExceptionCode.CHANNEL_NOT_REGISTERED) → HTTP 400.
  2. Worse, silently: DefaultPreferenceService.updatePreference delegates to IPreferenceRepository.upsertByUserAndChannel, whose Prisma implementation uses update: { enabled }. Saving a preference under a wrong channel does not error — it upserts a row for that (wrong) channel key, and on a subsequent call with the same wrong key overwrites the existing row's enabled rather than surfacing the mistake.

If your code accepts user-supplied channel names, validate them against the registry before persisting or sending:

import { BadRequestException, Inject } from '@nestjs/common';
import { DELIVERY_CHANNEL_REGISTRY, DeliveryChannelRegistry } from '@nathapp/nestjs-notify';

constructor(
  @Inject(DELIVERY_CHANNEL_REGISTRY) private readonly registry: DeliveryChannelRegistry,
) {}

setPreference(userId: string, tenantId: string, channel: string, enabled: boolean) {
  if (!this.registry.has(channel)) {
    throw new BadRequestException(`Unknown notification channel: ${channel}`);
  }
  return this.preferenceService.updatePreference(userId, tenantId, channel, enabled);
}

Preferences

A preference is keyed by @@unique([userId, tenantId, channel]), so there is at most one row per user, tenant, and channel. Each row carries an enabled: boolean and an optional metadata: Record<string, any>.

metadata is where consent state belongs. A boolean enabled alone cannot model consent-gated transports: WeChat mini-program subscribe messages, for example, require per-message user consent that can be pending, granted, or revoked, and often carries a consent timestamp or scope token. Store that in metadata alongside enabled.

Note: the shipped defaults ignore metadata on write — DefaultPreferenceService.updatePreference hardcodes metadata: {}, and the Prisma repository's upsertByUserAndChannel updates only { enabled }. To persist consent state in metadata, override the default preference service (and/or repository) with an implementation that accepts and writes it.

No controllers ship

This package exposes services, module registration, and interfaces only. It ships no controllers and no DTOs. Validating a channel name, authorizing the caller, shaping request bodies, and deciding which channels a user may configure are all the consumer's responsibility.

Queue processing

When enabling the notification processor, import the configured queue module inside NotifyModule so its QueueService can resolve in the notification module. A sibling import alone does not expose a non-global queue provider.

const queue = QueueModule.register({
  provider: QueueProviderType.BULLMQ,
  options: { connection: { host: 'localhost', port: 6379 } },
});

NotifyModule.register({
  enableProcessor: true,
  imports: [queue],
});

Import QueueModule and QueueProviderType from @nathapp/nestjs-queue. For asynchronous registration, pass the same module in NotifyModule.registerAsync({ imports: [queue], ... }).