@nkemtasoft/local-push
v0.1.0
Published
Local Web Push notification testing service — like Mailpit, but for VAPID web push
Maintainers
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-pushOpen 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=0npx
npx @nkemtasoft/local-pushClient SDK
Install the package in your project (typically as a dev dependency):
npm install -D @nkemtasoft/local-push
# or
yarn add -D @nkemtasoft/local-pushUsage
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=0This 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 TypeScriptLicense
MIT
