Push
Web Push notifications behind a transport contract: services send PushMessages through the concrete PushService, drivers deliver them to browser push subscriptions, and per-subscription failures come back in the result — including an expired flag telling you which subscriptions to prune.
Contract
@modularityjs/push defines the concrete PushService and the abstract PushTransport drivers implement:
class PushService {
send(message: PushMessage): Promise<PushSendResult>;
}
abstract class PushTransport {
// Delivers the message to every subscription in `to`. Per-subscription
// failures are reported in the result (rejected / rejections), never
// thrown — only transport-wide failures (bad credentials, no network
// stack) reject.
abstract send(message: PushMessage): Promise<PushSendResult>;
}PushService fills in configured defaults (ttl, urgency) when the message omits them, then delegates to the bound transport.
Message shape
// W3C PushSubscription-shaped delivery address (endpoint URL + encryption keys).
interface PushSubscription {
readonly endpoint: string;
readonly keys: { p256dh: string; auth: string };
readonly expirationTime?: number | null;
}
interface PushPayload {
readonly title?: string;
readonly body: string;
readonly icon?: string;
readonly url?: string; // click-through target URL
readonly data?: Record<string, unknown>;
}
interface PushMessage {
readonly to: PushSubscription | PushSubscription[];
readonly payload: PushPayload;
// Push-service retention in seconds when the device is offline.
readonly ttl?: number;
readonly urgency?: 'high' | 'low' | 'normal' | 'very-low';
// Replaces a pending, undelivered message carrying the same topic.
readonly topic?: string;
}
interface PushSendResult {
readonly accepted: string[]; // endpoints delivered to the push service
readonly rejected: string[]; // endpoints that failed
readonly rejections?: PushSendRejection[];
}
interface PushSendRejection {
readonly endpoint: string;
readonly statusCode?: number;
readonly message: string;
// True when the push service reported the subscription gone (404/410) — prune it.
readonly expired: boolean;
}Configuration
PushModule.forRoot() sets message defaults:
import { PushModule } from '@modularityjs/push';
PushModule.forRoot({ ttl: 86_400, urgency: 'normal' });| Option | Default | Description |
|---|---|---|
ttl | — | Default TTL (seconds) applied when a message omits its own |
urgency | — | Default urgency (very-low, low, normal, high) when message omits |
Drivers
Memory (@modularityjs/push-memory)
Buffers sent messages in memory and accepts every subscription. For development and testing — MemoryPushTransport.getSent() returns the buffered messages and clear() resets the buffer.
import { PushModule } from '@modularityjs/push';
import { PushMemoryModule } from '@modularityjs/push-memory';
const modules = [
PushModule,
PushMemoryModule,
// or with config:
PushMemoryModule.forRoot({ maxBuffer: 500 }),
];| Option | Default | Description |
|---|---|---|
maxBuffer | 10_000 | Maximum buffered messages — oldest entries drop past the cap |
Web Push (@modularityjs/push-webpush)
Delivers to real browser push services via the web-push library with VAPID authentication. Subscriptions fan out concurrently; each failure is isolated into a PushSendRejection, with expired: true on 404/410 responses.
import { PushModule } from '@modularityjs/push';
import { PushWebpushModule } from '@modularityjs/push-webpush';
const modules = [
PushModule,
PushWebpushModule.forRoot({
vapidPublicKey: process.env.VAPID_PUBLIC_KEY!,
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY!,
vapidSubject: 'mailto:ops@example.com',
}),
];| Option | Default | Description |
|---|---|---|
vapidPublicKey | — | VAPID public key (required) |
vapidPrivateKey | — | VAPID private key (required) |
vapidSubject | — | VAPID contact — a mailto: address or an https: URL (required) |
Generate the key pair once with the exported helper and store it in configuration — the public key is also what the browser needs for pushManager.subscribe:
import { generateVapidKeys } from '@modularityjs/push-webpush';
const { publicKey, privateKey } = generateVapidKeys();Usage
import { Inject, Injectable } from '@modularityjs/di';
import { PushService } from '@modularityjs/push';
import type { PushSubscription } from '@modularityjs/push';
@Injectable()
class AlertService {
constructor(@Inject(PushService) private readonly push: PushService) {}
async notifyDeploy(subscriptions: PushSubscription[]): Promise<void> {
const result = await this.push.send({
to: subscriptions,
payload: {
title: 'Deploy finished',
body: 'Version 2.4.0 is live.',
url: 'https://app.example.com/releases/2.4.0',
},
ttl: 3_600,
topic: 'deploy-status', // replaces a pending undelivered deploy message
});
// Prune subscriptions the push service reported gone (404/410).
for (const rejection of result.rejections ?? []) {
if (rejection.expired) {
await this.subscriptionStore.remove(rejection.endpoint);
}
}
}
}send resolves even when some subscriptions fail — check rejected / rejections for partial failures. Only transport-wide problems (bad VAPID credentials, no network stack) reject the promise.
Notification Channel
@modularityjs/notification-push plugs push into the multi-channel notification dispatch as the push channel. It delivers the envelope to every subscription in recipient.push via PushService (title = envelope subject, body = envelope body, data = envelope data) and skips recipients without push subscriptions — like recipients without an email or phone on the other channels.
import { NotificationModule } from '@modularityjs/notification';
import { NotificationPushModule } from '@modularityjs/notification-push';
import { PushModule } from '@modularityjs/push';
import { PushWebpushModule } from '@modularityjs/push-webpush';
const modules = [
PushModule,
PushWebpushModule.forRoot({
vapidPublicKey: '...',
vapidPrivateKey: '...',
vapidSubject: 'mailto:ops@example.com',
}),
NotificationModule,
NotificationPushModule, // registers the 'push' channel
];await notifications.send(
new OrderShipped(order.number, order.trackingUrl), // channels: ['mail', 'push']
{
email: customer.email,
push: customer.pushSubscriptions, // PushSubscription[]
},
);