Push
Send push notifications through Web Push, Firebase Cloud Messaging, or Expo. Pick one transport, build a PushMessage, and route to one device token or many.
The push channel delivers a notification through a single push transport — Web Push (VAPID), Firebase
Cloud Messaging, or Expo. You pick one transport at registration, build a fluent PushMessage, and
route to one device token (or subscription) or an array of them.
Install
Each transport pulls a different SDK as an optional peer — install only the one you use:
pnpm add @dudousxd/nestjs-notifications-push web-pushpnpm add @dudousxd/nestjs-notifications-push firebase-adminpnpm add @dudousxd/nestjs-notifications-push expo-server-sdkRegister the channel
Pick exactly one transport and supply its matching options:
import { PushChannelModule, WebPushTransport } from '@dudousxd/nestjs-notifications-push';
@Module({
imports: [
PushChannelModule.forRoot({
transport: WebPushTransport,
webPush: {
publicKey: process.env.VAPID_PUBLIC_KEY!,
privateKey: process.env.VAPID_PRIVATE_KEY!,
subject: 'mailto:ops@example.com',
},
}),
],
})
export class AppModule {}The target is a browser PushSubscription object (from PushManager.subscribe(), persisted
server-side).
import * as admin from 'firebase-admin';
import { FcmTransport, PushChannelModule } from '@dudousxd/nestjs-notifications-push';
@Module({
imports: [
PushChannelModule.forRoot({
transport: FcmTransport,
fcm: { credential: admin.credential.cert(serviceAccount) },
}),
],
})
export class AppModule {}The target is an FCM registration (device) token string. fcm options are forwarded to
admin.initializeApp; the transport reuses an already-initialized app if there is one.
import { ExpoTransport, PushChannelModule } from '@dudousxd/nestjs-notifications-push';
@Module({
imports: [
PushChannelModule.forRoot({
transport: ExpoTransport,
expo: { accessToken: process.env.EXPO_ACCESS_TOKEN },
}),
],
})
export class AppModule {}The target is an Expo push token string (e.g. ExponentPushToken[...]).
PushChannelModule.forRoot() takes:
| Option | Type | Default | Description |
|---|---|---|---|
transport | Type<PushTransport> | — | The transport class. Required — pick one. |
webPush | WebPushOptions | — | VAPID options. Supply with WebPushTransport. |
fcm | FcmOptions | — | Firebase Admin app options. Supply with FcmTransport. |
expo | ExpoOptions | — | Expo client options. Supply with ExpoTransport. |
apns | ApnsOptions | — | Apple credentials. Supply with ApnsTransport. |
resolveTransport | (tenant: string) => PushTransport | — | Per-tenant transport resolver. See Per-tenant config. |
onInvalidTokens | InvalidTokenCallback | — | Called with the dead device tokens a multicast send rejected, so you can prune them. See Multicast & invalid tokens. |
global | boolean | true | Register globally so the channel is discoverable app-wide. |
APNs (native iOS)
ApnsTransport delivers straight to Apple Push Notification service using token-based (.p8) auth.
The target is the device token string (or { deviceToken, topic }):
import { ApnsTransport, PushChannelModule } from '@dudousxd/nestjs-notifications-push';
PushChannelModule.forRoot({
transport: ApnsTransport,
apns: {
token: { key: process.env.APNS_KEY!, keyId: process.env.APNS_KEY_ID!, teamId: process.env.APPLE_TEAM_ID! },
topic: 'com.acme.app', // your bundle id
production: true,
},
});It pulls @parse/node-apn as an optional peer — install it only when you use APNs.
The notification side
Annotate the payload method with the @Push() handle and return a PushMessage:
import { type Notifiable, Notification } from '@dudousxd/nestjs-notifications-core';
import { Push, PushMessage } from '@dudousxd/nestjs-notifications-push';
@Notification()
export class OrderShipped {
constructor(private orderId: number) {}
@Push()
toPush({ notifiable }: ChannelContext): PushMessage {
return new PushMessage()
.title('Order shipped')
.body('Your order is on its way.')
.icon('https://app.example.com/icon.png')
.url(`https://app.example.com/orders/${this.orderId}`)
.data({ orderId: this.orderId });
}
}The Push handle also works as a via() token (via() { return [Push]; }) for
explicit routing; implement
PushNotification alongside the decorator for compile-time checks on toPush().
PushMessage builder
Each method returns this:
| Method | Effect |
|---|---|
.title(s) | Set the notification title. |
.body(s) | Set the body text. |
.icon(s) | Set the notification icon URL. |
.url(s) | Set the URL opened when the notification is tapped. |
.data(obj) | Attach an arbitrary data payload delivered alongside the notification. |
Transports map these fields to each provider's shape. FCM stringifies every data value (the FCM
API requires string data), and Expo delivers only title, body, and data.
Routing the target
The target comes from routeNotificationFor('push'). Return a single token/subscription, or an
array — the channel sends the message to each one:
export class User implements Notifiable {
constructor(public deviceTokens: string[]) {}
routeNotificationFor(channel: string) {
if (channel !== 'push') return undefined;
return this.deviceTokens; // one string, or an array of them
}
}The concrete shape of the target depends on the transport — a Web Push subscription object, an FCM device token, or an Expo push token.
Per-tenant config
In a multi-tenant app each tenant can push through its own project/credentials. Pass
resolveTransport — when a send is scoped to a tenant (via
forTenant(id) or a @Tenant() property), the channel uses the
PushTransport you return instead of the default:
PushChannelModule.forRoot({
transport: FcmTransport,
resolveTransport: (tenant) => tenantPushTransports.get(tenant) ?? defaultTransport,
});Sends with no tenant scope keep the default transport. See Multi-tenancy for the full picture.
Multicast & invalid tokens
When a recipient routes many device tokens (a string[]), transports that support multicast —
FCM and Expo — deliver them in a single batch via sendMany() instead of one call per token.
Transports without it (web-push, APNs) fall back to per-token send(). It's transparent to the
notification; you just get fewer provider round-trips.
Push providers also report tokens that are no longer valid (the app was uninstalled, the token
rotated). Hand the channel an onInvalidTokens callback to learn about them and prune your store:
PushChannelModule.forRoot({
transport: FcmTransport,
fcm: { /* … */ },
onInvalidTokens: async ({ notifiable, invalidTargets, tenant }) => {
await deviceTokens.removeAll(notifiable, invalidTargets); // stop pushing to dead tokens
},
});The callback receives an InvalidTokenReport ({ notifiable, invalidTargets, tenant? }). It fires
after a batch send when the provider flags rejected tokens — keeping your token list clean so
deliverability doesn't rot over time.
SMS
Text a notification through a pluggable SMS transport. Ships with a Twilio transport; return a plain string or a fluent SmsMessage, and route the recipient per notifiable.
Dispatchers
A dispatcher decides where and when a notification is processed — inline now, or on a worker later. The default is synchronous; opt into async per notification with shouldQueue and swap the driver in forRoot.