Skip to content

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:

typescript
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

typescript
// 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:

typescript
import { PushModule } from '@modularityjs/push';

PushModule.forRoot({ ttl: 86_400, urgency: 'normal' });
OptionDefaultDescription
ttlDefault TTL (seconds) applied when a message omits its own
urgencyDefault 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.

typescript
import { PushModule } from '@modularityjs/push';
import { PushMemoryModule } from '@modularityjs/push-memory';

const modules = [
  PushModule,
  PushMemoryModule,
  // or with config:
  PushMemoryModule.forRoot({ maxBuffer: 500 }),
];
OptionDefaultDescription
maxBuffer10_000Maximum 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.

typescript
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',
  }),
];
OptionDefaultDescription
vapidPublicKeyVAPID public key (required)
vapidPrivateKeyVAPID private key (required)
vapidSubjectVAPID 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:

typescript
import { generateVapidKeys } from '@modularityjs/push-webpush';

const { publicKey, privateKey } = generateVapidKeys();

Usage

typescript
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.

typescript
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
];
typescript
await notifications.send(
  new OrderShipped(order.number, order.trackingUrl), // channels: ['mail', 'push']
  {
    email: customer.email,
    push: customer.pushSubscriptions, // PushSubscription[]
  },
);