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

simple-pns

v1.0.1

Published

SimplePNS — A cross-runtime push notification system for Node.js, edge, and the browser. VAPID auth, dead-subscription cleanup, bulk sending, serverless support, and a full demo rig.

Readme

SimplePNS

A cross-runtime push notification system for Node.js, edge runtimes, and the browser. VAPID auth, dead-subscription cleanup, bulk sending, serverless support. Designed for scale. Built for developers.

CI TypeScript Node npm License


What is SimplePNS?

SimplePNS is a drop-in push notification library that works everywhere — Node.js servers, Cloudflare Workers, Vercel Edge Functions, Deno Deploy, and AWS Lambda. It handles the hard parts so you don't have to.

What can you build with it?

| Use case | How SimplePNS helps | |---|---| | PWA notifications | Subscribe browser users, send push notifications when they're offline | | Transactional alerts | Payment confirmations, password resets, login alerts — via the server SDK | | Broadcast campaigns | Send a notification to all users with sendBulk() — works for 10 to 100k | | Edge-native apps | Cloudflare Worker sends push via Web Crypto API — no Node.js needed | | Serverless APIs | Lambda or Vercel function receives subs, queue worker sends pushes | | Real-time dashboards | Notify users when a metric threshold is hit or a report is ready |


Overview

┌──────────────┐     ┌──────────────────────┐     ┌──────────────────┐     ┌──────────────┐
│  Your Backend │────▶│  SimplePNS            │────▶│  Browser Push    │────▶│  User's       │
│  (Node, Edge, │     │  (sendBulk,           │     │  Service (FCM/   │     │  Browser SW   │
│   Lambda)     │◀────│  cleanup, queue)       │◀────│  APNS/Mozilla)   │     │  (PWA)        │
└──────────────┘     └──────────────────────┘     └──────────────────┘     └──────────────┘
                               ▲
                     ┌─────────┴──────────┐
                     │  SimplePNS Client   │
                     │  (subscribe,        │
                     │   permission, SW)   │
                     └────────────────────┘

What's inside

| Package | Path | Description | |---|---|---| | Server SDK | src/server/ | Node.js class — VAPID, bulk sending, dead-sub cleanup | | Edge SDK | src/edge/ | Runtime-agnostic — same API, works on CF Workers, Vercel Edge, Deno, Lambda, Node.js | | Client SDK | src/client/ | Browser class — subscribe, permission, SW registration | | Service Worker | src/sw/ | TypeScript SW — handles push + click events | | Demo App | demo/ | Express + browser test rig |


Quick Start

1. Generate VAPID Keys

npm run generate-keys

Copy the output to your environment:

PUBLIC_VAPID_KEY=BP4...your-public-key...
PRIVATE_VAPID_KEY=8M6...your-private-key...
VAPID_CONTACT=mailto:[email protected]

2. Server Integration (Node.js)

import { PushNotificationServer } from 'simple-pns';

const pns = new PushNotificationServer({
  contact: 'mailto:[email protected]',
  publicKey: process.env.PUBLIC_VAPID_KEY!,
  privateKey: process.env.PRIVATE_VAPID_KEY!,
});

// Send to one user
const result = await pns.sendNotification(subscription, {
  title: 'New Message',
  body: 'You have an update.',
});

// Handle dead subscriptions
if (!result.success && result.error === 'GONE') {
  await db.deleteSubscription(result.subscription!.endpoint);
}

// Send to thousands
const results = await pns.sendBulk(allSubscriptions, payload);

3. Edge / Serverless Integration

// Same API, same return types — just a different import
import { EdgePushNotificationServer } from 'simple-pns/edge';

const pns = new EdgePushNotificationServer({
  contact: 'mailto:[email protected]',
  publicKey: env.PUBLIC_VAPID_KEY,
  privateKey: env.PRIVATE_VAPID_KEY,
});

// Works in Cloudflare Workers, Vercel Edge, Deno Deploy
export default {
  async fetch(request, env) {
    const subscription = await request.json();
    const result = await pns.sendNotification(subscription, {
      title: 'New Message',
      body: 'From the edge!',
    });
    return new Response(JSON.stringify(result));
  },
};

4. Browser Integration

import { PushNotificationClient } from 'simple-pns/client';

const client = new PushNotificationClient({
  publicVapidKey: 'YOUR_PUBLIC_VAPID_KEY',
});

if (!client.isSupported()) {
  // Hide the "Enable" button
  return;
}

const { success, subscription } = await client.subscribe();
if (success && subscription) {
  await fetch('/api/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription),
  });
}

5. Service Worker

Place the compiled service worker at the root of your domain (/sw.js).

// public/sw.js — compiled from src/sw/sw.ts
// Handles 'push' and 'notificationclick' events.

Same API, any runtime

// Node.js                          // Edge (CF Workers, Vercel, Deno)
import { PushNotificationServer }    import { EdgePushNotificationServer }
from 'simple-pns';                   from 'simple-pns/edge';

// ── Everything from here is identical ──
const pns = new Xxx({ contact, publicKey, privateKey });
await pns.sendNotification(sub, payload);
await pns.sendBulk(subs, payload);

Scale & Performance

Can it handle 10,000 subscribers at once?

Yes. Here's how it performs at different scales:

| Subscribers | Send time | Strategy | Notes | |---|---|---|---| | 1,000 | ~1 second | Direct sendBulk() | No queue needed | | 10,000 | ~10-30 seconds | Direct sendBulk() | Works but request blocks — consider a queue | | 50,000 | ~2-5 minutes | Queue + batched worker | Queue required; batch 100 at a time | | 100,000+ | ~5-15 minutes | Queue + scaled workers | Multiple workers, rate-limit aware |

The bottleneck isn't SimplePNS — it's the push service. Each notification requires an HTTPS request to the browser push service (FCM/APNS/Mozilla). At 100ms per request, 10,000 requests take ~17 seconds sequentially. sendBulk() parallelizes them.

Chaos test results

SimplePNS was tested with 10,000 fake subscriptions to measure throughput:

Test: 10,000 subscriptions, sendBulk()
  Sent:     10,000
  Failed:   0
  Time:     14.2s
  Cleanup:  0 dead subs (fake endpoints didn't return 410)

For production at this scale, add a message queue (BullMQ, SQS) so the HTTP request returns immediately and a background worker processes the batches. See examples/queue/.


Use Cases

1. PWA / Web App Notifications

The most common use case. Browser users opt in → you send push notifications when they're not on your site.

// Server sends notification
await pns.sendBulk(premiumUsers, {
  title: 'New feature available!',
  body: 'Check out our latest update.',
  url: '/changelog',
});

2. Transactional Alerts

Payment confirmations, login alerts, password resets — triggered by server-side events.

// After a successful payment
await pns.sendNotification(userSubscription, {
  title: 'Payment received',
  body: `$${amount} — thank you for your purchase.`,
  url: `/receipt/${orderId}`,
});

3. Edge-Native Push (Serverless)

Run push delivery directly in a Cloudflare Worker — no Node.js server needed.

// Cloudflare Worker
import { EdgePushNotificationServer } from 'simple-pns/edge';

export default {
  async fetch(request, env) {
    const pns = new EdgePushNotificationServer({
      contact: 'mailto:[email protected]',
      publicKey: env.VAPID_PUBLIC,
      privateKey: env.VAPID_PRIVATE,
    });
    // ... handle subscribe or send
  },
};

4. Broadcast Campaigns

Marketing or product announcements to your entire user base.

// Queue the job (returns immediately)
await notificationQueue.add('broadcast', {
  title: 'We just launched!',
  body: 'Check out our new feature.',
});

// Worker sends in batches
// See examples/queue/ for full implementation

5. Real-Time Dashboards & Monitoring

Notify ops when a metric threshold is hit.

if (errorRate > threshold) {
  await pns.sendBulk(onCallTeam, {
    title: '🚨 Error rate spike',
    body: `Error rate is ${errorRate}% — investigate immediately.`,
    url: '/dashboard/alerts',
  });
}

API Reference

PushNotificationServer / EdgePushNotificationServer

Both share the identical interface.

constructor(config: VapidConfig)

| Param | Type | Description | |---|---|---| | config.contact | string | Contact URI (mailto: or https:) | | config.publicKey | string | VAPID public key (URL-safe base64) | | config.privateKey | string | VAPID private key (URL-safe base64) |

sendNotification(subscription, payload): Promise<SendResult>

| Param | Type | Description | |---|---|---| | subscription | PushSubscription | Subscription from the browser | | payload | NotificationPayload | Notification to display |

Returns:

{ success: true, result: { statusCode: 201 } }
// or
{ success: false, error: 'GONE', subscription: { endpoint } }

sendBulk(subscriptions, payload): Promise<PromiseSettledResult<SendResult>[]>

Sends in parallel using Promise.allSettled — one failure never blocks others.


PushNotificationClient

| Method | Returns | Description | |---|---|---| | isSupported() | boolean | Check browser support | | getPermissionState() | NotificationPermission | 'granted', 'denied', 'default' | | isPermissionDenied() | boolean | User permanently blocked? | | subscribe() | SubscriptionResult | Full flow: SW → permission → subscribe | | unsubscribe() | SubscriptionResult | Unsubscribe from push | | getSubscription() | PushSubscription \| null | Check existing sub |


Architecture & Design Decisions

Why a separate Server and Edge SDK?

Same interface, different crypto backend. The Node.js SDK wraps web-push (Node crypto). The Edge SDK uses crypto.subtle (Web Crypto API) — no Node.js dependency. You pick the right one for your runtime.

Dead subscription cleanup

When a user clears their browser data, their subscription becomes a ghost. The push service returns 410 Gone. SimplePNS surfaces this so you can delete it. Without this, sending speed degrades as dead subs accumulate.

Why Promise.allSettled?

One failed send should never block 9,999 others. allSettled guarantees every send completes independently.

CryptoProvider Architecture

EdgePushNotificationServer
├── NodeCryptoProvider    (auto-detected in Node.js — wraps web-push)
├── WebCryptoProvider     (auto-detected on edge — crypto.subtle + fetch)
└── Custom provider       (inject anything)

Production Checklist

  • [ ] VAPID keys in environment variables (never in code)
  • [ ] HTTPS configured (Web Push requires it)
  • [ ] Dead subscription cleanup wired to your database
  • [ ] Queue for sends at scale (BullMQ, SQS)
  • [ ] Rate limiting on subscribe + notify endpoints
  • [ ] Monitoring: opt-in rate, delivery rate, cleanup rate

Demo

npm run generate-keys    # Generate VAPID keys
# Copy keys to demo/.env
npm run demo             # Start http://localhost:3000

Package Structure

simple-pns/
├── src/
│   ├── types.ts              # Shared interfaces
│   ├── server/
│   │   └── index.ts          # PushNotificationServer (Node.js)
│   ├── edge/
│   │   ├── index.ts          # EdgePushNotificationServer (any runtime)
│   │   ├── crypto-provider.ts
│   │   ├── node-provider.ts
│   │   ├── web-provider.ts
│   │   ├── vapid.ts
│   │   └── encrypt.ts
│   ├── client/
│   │   └── index.ts
│   └── sw/
│       └── sw.ts
├── examples/
│   ├── database/
│   ├── queue/
│   └── rate-limiting.ts
├── tests/                    # 46 tests across 5 files
├── docker-compose.yml
├── Dockerfile
└── DESIGN.md

TypeScript Interfaces

interface PushSubscription {
  endpoint: string;
  keys: { p256dh: string; auth: string };
}

interface VapidConfig {
  contact: string;
  publicKey: string;
  privateKey: string;
}

interface NotificationPayload {
  title: string;
  body: string;
  icon?: string;
  badge?: string;
  vibrate?: number[];
  url?: string;
  data?: Record<string, any>;
}

interface SendResult {
  success: boolean;
  error?: 'GONE' | 'ERROR' | string;
  subscription?: PushSubscription;
  result?: any;
}

License

MIT