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

@nkemtasoft/local-push

v0.1.0

Published

Local Web Push notification testing service — like Mailpit, but for VAPID web push

Readme

local-push

Local Web Push notification testing service — like Mailpit but for VAPID web push.

Run it locally or in Docker, point your push subscriptions at it, and inspect every notification your backend sends — zero changes to your application code.


How it works

The web-push npm library encrypts your notification payload and POSTs it to whatever URL is in the subscription's endpoint field. local-push generates subscriptions whose endpoints point to itself, so it intercepts, decrypts, stores, and displays every push notification — no browser or third-party push service required.

┌──────────────┐   webpush.sendNotification()   ┌──────────────┐
│  Your        │ ──────────────────────────────► │  local-push  │
│  Backend     │   (encrypted POST to endpoint)  │  Server      │
└──────────────┘                                 └──────┬───────┘
                                                        │
                                                 decrypt & store
                                                        │
                                                 ┌──────▼───────┐
                                                 │  Dashboard   │
                                                 │  + REST API  │
                                                 └──────────────┘

Quick Start

Docker (recommended)

docker run -p 4078:4078 ghcr.io/nkemtasoft/local-push

Open https://localhost:4078 in your browser to see the dashboard.

Docker Compose

services:
  local-push:
    image: ghcr.io/nkemtasoft/local-push
    ports:
      - "4078:4078"

  your-backend:
    # ...
    environment:
      # Accept the self-signed TLS certificate
      - NODE_TLS_REJECT_UNAUTHORIZED=0

npx

npx @nkemtasoft/local-push

Client SDK

Install the package in your project (typically as a dev dependency):

npm install -D @nkemtasoft/local-push
# or
yarn add -D @nkemtasoft/local-push

Usage

import { LocalWebPushClient } from "@nkemtasoft/local-push/client";

const client = new LocalWebPushClient({ url: "https://localhost:4078" });

// Create a subscription (returns standard PushSubscription format)
const { subscription, id } = await client.createSubscription();
// → { endpoint: "https://localhost:4078/push/uuid", keys: { p256dh: "...", auth: "..." } }

// Insert into your database for the test user
await db.webpushSubscriptions.create({
  user: testUserId,
  endpoint: subscription.endpoint,
  keys: subscription.keys,
});

// Your backend sends push notifications as normal — no code changes needed
await yourBackend.sendNotification(testUserId, { title: "Hello" });

// Check what was received
const msg = await client.waitForMessage({ timeout: 5000 });
console.log(msg.payload); // { title: "Hello" }

SDK API

// Subscriptions
client.createSubscription(baseUrl?: string): Promise<CreateSubscriptionResponse>
client.listSubscriptions(): Promise<SubscriptionInfo[]>
client.getSubscription(id: string): Promise<SubscriptionInfo>
client.deleteSubscription(id: string): Promise<void>
client.deleteAllSubscriptions(): Promise<void>

// Messages
client.getMessages(filter?: MessageFilter): Promise<MessageListResponse>
client.getMessage(id: string): Promise<StoredMessage>
client.deleteMessage(id: string): Promise<void>
client.deleteAllMessages(): Promise<void>

// Testing utilities
client.waitForMessage(options?: WaitOptions): Promise<StoredMessage>
client.reset(): Promise<void>

waitForMessage polls the API until a new message arrives or the timeout is reached. Useful in integration tests:

const msgPromise = client.waitForMessage({ timeout: 5000 });
await webpush.sendNotification(subscription, JSON.stringify(payload));
const msg = await msgPromise;

expect(msg.payload).toEqual(payload);

REST API

All endpoints are under /api/v1.

| Method | Path | Description | |--------|------|-------------| | GET | /api/v1/health | Health check | | POST | /api/v1/subscriptions | Create a subscription | | GET | /api/v1/subscriptions | List all subscriptions | | GET | /api/v1/subscriptions/:id | Get a subscription | | DELETE | /api/v1/subscriptions/:id | Delete a subscription | | DELETE | /api/v1/subscriptions | Delete all subscriptions | | GET | /api/v1/messages | List messages (?subscriptionId=&limit=&offset=) | | GET | /api/v1/messages/:id | Get a message | | DELETE | /api/v1/messages/:id | Delete a message | | DELETE | /api/v1/messages | Delete all messages | | GET | /api/v1/events | SSE stream (real-time updates) |

Create Subscription

curl -k -X POST https://localhost:4078/api/v1/subscriptions \
  -H "Content-Type: application/json" \
  -d '{"baseUrl": "https://localhost:4078"}'

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "subscription": {
    "endpoint": "https://localhost:4078/push/550e8400-e29b-41d4-a716-446655440000",
    "keys": {
      "p256dh": "BLa5...",
      "auth": "kQ3..."
    }
  },
  "createdAt": "2025-01-01T00:00:00.000Z"
}

Dashboard

The built-in web dashboard (served at the root URL) provides:

  • Real-time message list with auto-updates via SSE
  • Click-to-inspect with full JSON payload, headers, and metadata
  • Subscription filter
  • Clear all button
  • Dark/light mode (follows system preference)

Configuration

| Environment Variable | Default | Description | |---------------------|---------|-------------| | PORT | 4078 | Server port | | MAX_MESSAGES | 1000 | Maximum stored messages (FIFO eviction) | | BASE_URL | auto-detect | Base URL for generated subscription endpoints | | LOG_LEVEL | info | Log verbosity |

TLS / Self-Signed Certificate

The web-push npm library always uses HTTPS, so local-push generates a self-signed TLS certificate on startup.

For your backend services, set the environment variable:

NODE_TLS_REJECT_UNAUTHORIZED=0

This is standard practice for local development and requires no code changes to your application.

For browsers, you'll see a certificate warning when opening the dashboard — click through it to proceed.

Integration Example

Here's a full integration test using web-push and vitest:

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import webpush from "web-push";
import { LocalWebPushClient } from "@nkemtasoft/local-push/client";

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

const client = new LocalWebPushClient({ url: "https://localhost:4078" });

beforeAll(() => {
  webpush.setVapidDetails(
    "mailto:[email protected]",
    process.env.VAPID_PUBLIC_KEY!,
    process.env.VAPID_PRIVATE_KEY!,
  );
});

describe("push notifications", () => {
  it("sends and receives a notification", async () => {
    await client.reset();
    const { subscription } = await client.createSubscription();

    const payload = { title: "Hello", body: "World" };

    const msgPromise = client.waitForMessage({ timeout: 5000 });
    await webpush.sendNotification(subscription, JSON.stringify(payload));
    const msg = await msgPromise;

    expect(msg.payload).toEqual(payload);
  });
});

Development

git clone https://github.com/nkemtasoft/local-push.git
cd local-push
yarn install
yarn dev      # Start with hot reload
yarn test     # Run tests
yarn build    # Compile TypeScript

License

MIT