Skip to main content
Technology & EngineeringNotification Services205 lines

Web Push

Implement Web Push API with service workers for browser push notifications.

Quick Summary27 lines
You are a web notification engineer who implements the Web Push API with service workers. Web Push is a W3C standard that enables servers to send push notifications to browsers even when the web app is closed. It uses VAPID (Voluntary Application Server Identification) for server authentication, the Push API for subscription management, and service workers for background message handling. Unlike vendor-specific solutions, Web Push works natively in Chrome, Firefox, Edge, and Safari without third-party SDKs.

## Key Points

- **Regenerating VAPID keys**: Keys must be stable; regenerating them invalidates all existing subscriptions.
- **Not handling 410 responses**: Stale subscriptions accumulate; always delete subscriptions when the push service returns 410 Gone.
- **Skipping `showNotification`**: Browsers require a visible notification on push; silent pushes are blocked or throttled.
- **Large payloads**: Web Push payloads are limited to ~4 KB; send identifiers and let the client fetch details if needed.
- Web applications needing push notifications without third-party services
- Progressive Web Apps (PWAs) requiring offline-capable notifications
- Projects where vendor independence and standards compliance matter
- Blogs, news sites, or e-commerce sites for re-engagement notifications
- Applications already using service workers for caching or background sync

## Quick Example

```bash
npm install web-push
```

```env
VAPID_PUBLIC_KEY=BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkOs-WNpObGKic6x_5fFg0hp4
VAPID_PRIVATE_KEY=UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls
VAPID_SUBJECT=mailto:admin@example.com
```
skilldb get notification-services-skills/Web PushFull skill: 205 lines
Paste into your CLAUDE.md or agent config

Web Push API

You are a web notification engineer who implements the Web Push API with service workers. Web Push is a W3C standard that enables servers to send push notifications to browsers even when the web app is closed. It uses VAPID (Voluntary Application Server Identification) for server authentication, the Push API for subscription management, and service workers for background message handling. Unlike vendor-specific solutions, Web Push works natively in Chrome, Firefox, Edge, and Safari without third-party SDKs.

Core Philosophy

Standards-Based, No Vendor Lock-In

Web Push is a browser standard. Your push subscriptions are tied to the browser's push service (FCM for Chrome, Mozilla's push service for Firefox, APNs for Safari), but your server code is provider-agnostic. The web-push library handles encryption and delivery to any standards-compliant push service. No vendor dashboard, no monthly fees, no SDK — just HTTP and encryption.

Service Worker as the Notification Engine

Push messages are received by a service worker, not by your page JavaScript. The service worker runs in the background and must display a notification when a push event arrives — browsers enforce this requirement. Design your push payload to contain everything the service worker needs to render the notification without making additional network requests.

Subscription Lifecycle is Your Responsibility

The browser provides a PushSubscription object containing an endpoint URL and encryption keys. Your server must store these subscriptions and handle their lifecycle: subscriptions expire, users revoke permission, and browsers change endpoints. Always re-subscribe on page load and prune invalid subscriptions when pushes fail with 410 Gone responses.

Setup

Install

npm install web-push

Environment Variables

VAPID_PUBLIC_KEY=BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkOs-WNpObGKic6x_5fFg0hp4
VAPID_PRIVATE_KEY=UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls
VAPID_SUBJECT=mailto:admin@example.com

Key Patterns

1. Generate VAPID Keys and Configure Server

Do:

import webpush from "web-push";

// Generate keys once: npx web-push generate-vapid-keys
webpush.setVapidDetails(
  process.env.VAPID_SUBJECT!,
  process.env.VAPID_PUBLIC_KEY!,
  process.env.VAPID_PRIVATE_KEY!,
);

async function sendPush(subscription: webpush.PushSubscription, payload: object) {
  try {
    await webpush.sendNotification(subscription, JSON.stringify(payload));
  } catch (err: any) {
    if (err.statusCode === 410 || err.statusCode === 404) {
      await removeSubscription(subscription.endpoint);
    }
  }
}

Not this:

// Generating new VAPID keys on every server restart
const vapidKeys = webpush.generateVAPIDKeys(); // New keys = all existing subscriptions break
webpush.setVapidDetails("mailto:a@b.com", vapidKeys.publicKey, vapidKeys.privateKey);

2. Client-Side Subscription

Do:

async function subscribeToPush(): Promise<PushSubscription | null> {
  const registration = await navigator.serviceWorker.ready;
  const permission = await Notification.requestPermission();
  if (permission !== "granted") return null;

  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!),
  });

  await fetch("/api/push/subscribe", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(subscription),
  });

  return subscription;
}

function urlBase64ToUint8Array(base64String: string): Uint8Array {
  const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
  const raw = atob(base64);
  return Uint8Array.from([...raw].map((char) => char.charCodeAt(0)));
}

Not this:

// Subscribing without checking permission or sending to server
const sub = await registration.pushManager.subscribe({ userVisibleOnly: true });
// Subscription never reaches server; lost on page close

3. Service Worker Push Handler

Do:

// sw.ts (service worker)
self.addEventListener("push", (event: PushEvent) => {
  const data = event.data?.json() ?? { title: "Notification", body: "" };

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: data.icon ?? "/icon-192.png",
      badge: "/badge-72.png",
      data: { url: data.url },
      tag: data.tag,
      renotify: !!data.tag,
    })
  );
});

self.addEventListener("notificationclick", (event: NotificationEvent) => {
  event.notification.close();
  const url = event.notification.data?.url ?? "/";
  event.waitUntil(clients.openWindow(url));
});

Not this:

// Not calling showNotification — browser will show generic "This site has been updated"
self.addEventListener("push", (event) => {
  console.log("Push received:", event.data?.text());
  // Must show a notification; browsers enforce this
});

Common Patterns

Register Service Worker

if ("serviceWorker" in navigator && "PushManager" in window) {
  const registration = await navigator.serviceWorker.register("/sw.js", { scope: "/" });
  console.log("SW registered:", registration.scope);
}

Re-subscribe on Page Load

async function syncSubscription(userId: string) {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.getSubscription();

  if (subscription) {
    await fetch("/api/push/subscribe", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ userId, subscription }),
    });
  }
}

Notification with Actions

// In service worker
self.registration.showNotification("New Message", {
  body: "Alice sent you a message",
  actions: [
    { action: "reply", title: "Reply" },
    { action: "dismiss", title: "Dismiss" },
  ],
  data: { messageId: "msg-123", conversationUrl: "/chat/alice" },
});

self.addEventListener("notificationclick", (event: NotificationEvent) => {
  event.notification.close();
  if (event.action === "reply") {
    event.waitUntil(clients.openWindow(`/chat/alice?reply=true`));
  }
});

Anti-Patterns

  • Regenerating VAPID keys: Keys must be stable; regenerating them invalidates all existing subscriptions.
  • Not handling 410 responses: Stale subscriptions accumulate; always delete subscriptions when the push service returns 410 Gone.
  • Skipping showNotification: Browsers require a visible notification on push; silent pushes are blocked or throttled.
  • Large payloads: Web Push payloads are limited to ~4 KB; send identifiers and let the client fetch details if needed.

When to Use

  • Web applications needing push notifications without third-party services
  • Progressive Web Apps (PWAs) requiring offline-capable notifications
  • Projects where vendor independence and standards compliance matter
  • Blogs, news sites, or e-commerce sites for re-engagement notifications
  • Applications already using service workers for caching or background sync

Install this skill directly: skilldb add notification-services-skills

Get CLI access →