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

@pushforge/builder

v2.0.5

Published

Zero-dependency Web Push library for Cloudflare Workers, Vercel Edge, Convex, Deno, Bun, and Node.js 20+. A web-push alternative that uses Web Crypto API instead of Node.js crypto.

Readme

PushForge Builder

A lightweight, dependency-free Web Push library built on the standard Web Crypto API.

npm version npm downloads License: MIT TypeScript

Send push notifications from any JavaScript runtime · Zero dependencies

GitHub · npm · Report Bug

Try the Playground →


npm install @pushforge/builder

Playground

Test PushForge in your browser at pushforge.draphy.org — an interactive playground for testing push notifications, powered by Cloudflare Workers.

  • Quick Test — enable notifications, send a test message, see it arrive in real time
  • Topic Channels — test targeted notifications by subscribing to specific channels
  • Notification Customization — experiment with title, body, icon, image, action buttons, vibration, click URL
  • Push Options — test urgency levels (battery hints) and TTL (message expiry)
  • Cross-Browser — test across Chrome, Firefox, Edge, Safari 16+
  • Subscriptions auto-expire (5 min for quick test, 1 hour for topics) — no permanent data stored
  • The backend is a single Cloudflare Worker using buildPushHTTPRequest() with zero dependencies

Why PushForge?

| | PushForge | web-push | |---|:---:|:---:| | Dependencies | 0 | 5+ (with nested deps) | | Cloudflare Workers | Yes | No | | Vercel Edge | Yes | No | | Convex | Yes* | No | | Deno / Bun | Yes | Limited | | TypeScript | First-class | @types package |

* Convex requires "use node"; directive. See example.

Traditional web push libraries rely on Node.js-specific APIs (crypto.createECDH, https.request) that don't work in modern edge runtimes. PushForge uses the standard Web Crypto API, making it portable across all JavaScript environments.

Quick Start

1. Generate VAPID Keys

npx @pushforge/builder vapid

This outputs a public key (for your frontend) and a private key in JWK format (for your server).

2. Subscribe Users (Frontend)

Use the VAPID public key to subscribe users to push notifications:

// In your frontend application
const registration = await navigator.serviceWorker.ready;

const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: 'YOUR_VAPID_PUBLIC_KEY' // From step 1
});

// Send this subscription to your server
// subscription.toJSON() returns:
// {
//   endpoint: "https://fcm.googleapis.com/fcm/send/...",
//   keys: {
//     p256dh: "BNcRd...",
//     auth: "tBHI..."
//   }
// }
await fetch('/api/subscribe', {
  method: 'POST',
  body: JSON.stringify(subscription)
});

3. Send Notifications (Server)

import { buildPushHTTPRequest } from "@pushforge/builder";

// Your VAPID private key (JWK format from step 1)
const privateJWK = {
  kty: "EC",
  crv: "P-256",
  x: "...",
  y: "...",
  d: "..."
};

// The subscription object from the user's browser
const subscription = {
  endpoint: "https://fcm.googleapis.com/fcm/send/...",
  keys: {
    p256dh: "BNcRd...",
    auth: "tBHI..."
  }
};

// Build and send the notification
const { endpoint, headers, body } = await buildPushHTTPRequest({
  privateJWK,
  subscription,
  message: {
    payload: {
      title: "New Message",
      body: "You have a new notification!",
      icon: "/icon.png"
    },
    adminContact: "mailto:[email protected]"
  }
});

const response = await fetch(endpoint, {
  method: "POST",
  headers,
  body
});

if (response.status === 201) {
  console.log("Notification sent");
}

Understanding Push Subscriptions

When a user subscribes to push notifications, the browser returns a PushSubscription object:

{
  // The unique URL for this user's browser push service
  endpoint: "https://fcm.googleapis.com/fcm/send/dAPT...",

  keys: {
    // Public key for encrypting messages (base64url)
    p256dh: "BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA...",

    // Authentication secret (base64url)
    auth: "tBHItJI5svbpez7KI4CCXg=="
  }
}

| Field | Description | |-------|-------------| | endpoint | The push service URL. Each browser vendor has their own (Google FCM, Mozilla autopush, Apple APNs). | | p256dh | The user's public key for ECDH P-256 message encryption. | | auth | A shared 16-byte authentication secret. |

Store these securely on your server. You'll need them to send notifications to this user.

API Reference

buildPushHTTPRequest(options)

Builds an HTTP request for sending a push notification.

const { endpoint, headers, body } = await buildPushHTTPRequest({
  privateJWK,    // Your VAPID private key (JWK object or JSON string)
  subscription,  // User's push subscription
  message: {
    payload,       // Any JSON-serializable data
    adminContact,  // Contact email (mailto:...) or URL
    options: {     // Optional
      ttl,         // Time-to-live in seconds (default: 86400, max: 86400)
      urgency,     // "very-low" | "low" | "normal" | "high"
      topic        // Topic for notification replacement
    }
  }
});

Returns: { endpoint: string, headers: Headers, body: ArrayBuffer }

Parameters

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | privateJWK | JsonWebKey | string | Yes | Your VAPID private key (JWK object or JSON string) | | subscription | PushSubscription | Yes | User's push subscription with endpoint and keys | | message.payload | any | Yes | Any JSON-serializable data to send (see Notification Payload) | | message.adminContact | string | Yes | Contact for push service (mailto:[email protected] or URL) | | message.options | object | No | Push delivery options (see below) |

Push Options (Web Push Protocol Headers)

These options control how the push service handles message delivery:

| Option | Type | Default | Description | |--------|------|---------|-------------| | ttl | number | 86400 | Time-to-live in seconds. How long the push service retains the message if user is offline. Max 24 hours. | | urgency | string | - | Battery hint: "very-low" (ads), "low" (topic updates), "normal" (chat), "high" (calls/time-sensitive). | | topic | string | - | Topic identifier. New message with same topic replaces pending one at push service level (before delivery). |

TypeScript Types

For TypeScript users, these types are exported:

import type { 
  BuilderOptions,    // Parameter type for buildPushHTTPRequest
  PushMessage,       // The message object type
  PushSubscription   // The subscription object type
} from "@pushforge/builder";

Platform Examples

Cloudflare Workers

export default {
  async fetch(request, env) {
    const subscription = await request.json();

    const { endpoint, headers, body } = await buildPushHTTPRequest({
      privateJWK: JSON.parse(env.VAPID_PRIVATE_KEY),
      subscription,
      message: {
        payload: { title: "Hello from the Edge!" },
        adminContact: "mailto:[email protected]"
      }
    });

    return fetch(endpoint, { method: "POST", headers, body });
  }
};

Vercel Edge Functions

import { buildPushHTTPRequest } from "@pushforge/builder";

export const config = { runtime: "edge" };

export default async function handler(request: Request) {
  const subscription = await request.json();

  const { endpoint, headers, body } = await buildPushHTTPRequest({
    privateJWK: JSON.parse(process.env.VAPID_PRIVATE_KEY!),
    subscription,
    message: {
      payload: { title: "Edge Notification" },
      adminContact: "mailto:[email protected]"
    }
  });

  await fetch(endpoint, { method: "POST", headers, body });
  return new Response("Sent", { status: 200 });
}

Convex

Note: Convex's default runtime doesn't support ECDH operations required by Web Push. Add "use node"; to use the Node.js runtime.

"use node";

import { action } from "./_generated/server";
import { buildPushHTTPRequest } from "@pushforge/builder";
import { v } from "convex/values";

export const sendPush = action({
  args: { subscription: v.any(), title: v.string(), body: v.string() },
  handler: async (ctx, { subscription, title, body }) => {
    const { endpoint, headers, body: reqBody } = await buildPushHTTPRequest({
      privateJWK: JSON.parse(process.env.VAPID_PRIVATE_KEY!),
      subscription,
      message: {
        payload: { title, body },
        adminContact: "mailto:[email protected]"
      }
    });

    await fetch(endpoint, { method: "POST", headers, body: reqBody });
  }
});

Deno

import { buildPushHTTPRequest } from "npm:@pushforge/builder";

const { endpoint, headers, body } = await buildPushHTTPRequest({
  privateJWK: JSON.parse(Deno.env.get("VAPID_PRIVATE_KEY")!),
  subscription,
  message: {
    payload: { title: "Hello from Deno!" },
    adminContact: "mailto:[email protected]"
  }
});

await fetch(endpoint, { method: "POST", headers, body });

Bun

import { buildPushHTTPRequest } from "@pushforge/builder";

const { endpoint, headers, body } = await buildPushHTTPRequest({
  privateJWK: JSON.parse(Bun.env.VAPID_PRIVATE_KEY!),
  subscription,
  message: {
    payload: { title: "Hello from Bun!" },
    adminContact: "mailto:[email protected]"
  }
});

await fetch(endpoint, { method: "POST", headers, body });

How It Works

Your Server (PushForge) → Push Service (FCM/APNs) → Service Worker → User's Device

PushForge handles:

  • Encrypts payload
  • Signs with VAPID
  • Sets ttl/urgency/topic headers

Your service worker handles:

  • Displays notification (title, body, icon, actions, etc.)
  • Handles clicks

Notification Payload

The payload field accepts any JSON-serializable data — PushForge encrypts and delivers it as-is. Your service worker receives this payload and passes it to the browser's showNotification() API.

Note: These are standard Web Notifications API options, not PushForge-specific. PushForge handles the transport; your service worker handles the display.

Common fields:

| Field | Type | Description | |-------|------|-------------| | title | string | Notification title (required) | | body | string | Notification body text | | icon | string | URL for the notification icon | | badge | string | URL for the badge (small monochrome icon) | | image | string | URL for a large image | | dir | string | Text direction: "auto", "ltr", or "rtl" | | lang | string | Language tag (e.g., "en-US", "es") | | tag | string | Tag for notification replacement (same tag = replace, not stack) | | renotify | boolean | Vibrate/alert again when replacing a notification with same tag | | requireInteraction | boolean | Keep notification visible until user interacts | | silent | boolean | Suppress sound and vibration | | timestamp | number | Timestamp in milliseconds (e.g., Date.now()) | | vibrate | number[] | Vibration pattern [vibrate, pause, vibrate, ...] | | actions | array | Action buttons (max 2): [{ action: "id", title: "Label", icon?: "url" }] | | data | object | Custom data (e.g., { url: "/page" } for click handling) |

Example with full options:

const { endpoint, headers, body } = await buildPushHTTPRequest({
  privateJWK,
  subscription,
  message: {
    payload: {
      title: "New Message",
      body: "John: Hey, are you free?",
      icon: "/icons/chat.png",
      badge: "/icons/badge.png",
      image: "/images/preview.jpg",
      tag: "chat-john",
      renotify: true,
      actions: [
        { action: "reply", title: "Reply" },
        { action: "dismiss", title: "Dismiss" }
      ],
      data: { url: "/chat/john", messageId: "123" }
    },
    adminContact: "mailto:[email protected]",
    options: { urgency: "high", ttl: 3600 }
  }
});

Service Worker Setup

Handle incoming push notifications in your service worker:

// sw.js
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {};

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: data.icon,
      badge: data.badge,
      image: data.image,
      dir: data.dir,
      lang: data.lang,
      tag: data.tag,
      renotify: data.renotify,
      requireInteraction: data.requireInteraction,
      silent: data.silent,
      timestamp: data.timestamp,
      vibrate: data.vibrate,
      actions: data.actions,
      data: data.data
    })
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();

  // Handle action button clicks
  if (event.action === 'reply') {
    clients.openWindow('/chat?action=reply');
    return;
  }

  // Handle main notification click
  const url = event.notification.data?.url || '/';
  event.waitUntil(clients.openWindow(url));
});

Requirements

Node.js 20+ or any runtime with Web Crypto API support.

| Environment | Status | |-------------|--------| | Node.js 20+ | Fully supported | | Cloudflare Workers | Fully supported | | Vercel Edge | Fully supported | | Deno | Fully supported | | Bun | Fully supported | | Convex | Requires "use node"; (example) | | Modern Browsers | Fully supported |

import { webcrypto } from "node:crypto";
globalThis.crypto = webcrypto;

import { buildPushHTTPRequest } from "@pushforge/builder";

Or: node --experimental-global-webcrypto your-script.js

Security

PushForge validates all inputs before processing:

  • VAPID key structure (EC P-256 curve with required x, y, d parameters)
  • Subscription endpoint (must be valid HTTPS URL)
  • p256dh key format (65-byte uncompressed P-256 point)
  • Auth secret length (exactly 16 bytes)
  • Payload size (max 4KB per Web Push spec)
  • TTL bounds (max 24 hours per VAPID spec)

Contributing

Contributions welcome! See CONTRIBUTING.md for guidelines.

License

MIT © David Raphi