easy-pingv0.7.0

Get in touch

Questions, bug reports, or anything about easy-ping. Either of these reaches me.

Emailteklumo.jembere@gmail.comTelegram@teklumt

For anything others would benefit from, a GitHub issue is better than a DM, because it's searchable.

GitHub

Channels / Web push

Edit this page

Web push

VAPID and aes128gcm on Web Crypto — no node:crypto, so it runs on Workers and Edge, not only Node.

Web push is a plugin (push) plus a provider (webPush), because the channel needs a device registry the core schema doesn't have — see Push devices for that table and its routes.

Generating VAPID keys

One keypair, generated once and stored as secrets — never regenerated per deploy, or every existing subscription breaks.

import { generateVapidKeys } from "easy-ping/providers/web-push";

const vapid = await generateVapidKeys();
// { publicKey: "...", privateKey: "..." } — save both

Provider and plugin

notify.ts
import { push } from "easy-ping/plugins/push";
import { webPush } from "easy-ping/providers/web-push";

const pushPlugin = push({
  provider: webPush({
    subject: "mailto:ops@acme.dev",  // or an https: URL
    vapid: { publicKey: process.env.VAPID_PUBLIC_KEY!, privateKey: process.env.VAPID_PRIVATE_KEY! },
  }),
  render: ({ type, payload }) => {
    const data = payload as { title?: string; body?: string };
    return { title: data.title ?? type, body: data.body ?? "You have a new notification" };
  },
});

export const notify = easyPing({
  // ...
  plugins: [pushPlugin],
});

render builds what actually shows up in the OS notification. It receives the same typed payload your notification's schema validates.

The service worker

This is the one piece the library genuinely cannot supply — it has to run in your app's own origin. Without a push listener, the browser shows its own generic "site updated" text instead of your title and body.

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

  event.waitUntil(
    self.registration.showNotification(payload.title ?? "Notification", {
      body: payload.body ?? "",
      data: payload.data ?? {},
      tag: payload.data?.notificationId,
    }),
  );
});

self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  event.waitUntil(self.clients.openWindow("/"));
});

Subscribing a browser

components/EnablePush.tsx
import { subscribeToPush, isPushSupported } from "easy-ping/browser";

async function enable() {
  if (!isPushSupported()) return;

  const { vapidPublicKey } = await fetch("/api/config").then((r) => r.json());

  await subscribeToPush({
    publicKey: vapidPublicKey,
    baseUrl: "/api/notifications",
  });
}

This registers the service worker (if not already registered), requests the notification permission, calls pushManager.subscribe, and POSTs the resulting subscription to /push/devices — one call covers the whole browser-side flow.

Dead endpoints

When a push service reports an endpoint gone (404/410 — the user uninstalled, cleared site data, or the subscription expired), the provider surfaces { expired: true } and the device row is deleted automatically on the next delivery attempt. An unpruned registry accumulates dead subscriptions forever, and every send slows down fanning out to endpoints that will never accept anything again.

Platform caveats

Secure context

localhost counts as secure, so push works there without HTTPS. Any other host needs real TLS — the service worker will refuse to register otherwise.

iOS

Web push only reaches an iOS PWA installed to the home screen (Safari 16.4+). Desktop Safari 16+, and Chrome/Edge/Firefox everywhere, work as a normal browser tab.