Web Push
Implement Web Push API with service workers for browser push notifications.
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 linesWeb 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
Related Skills
Apple Push Notification
Integrate Apple Push Notification service (APNs) for iOS, macOS, and Safari
Courier
Integrate Courier notification orchestration for multi-channel message routing.
Engagespot
Engagespot is a multi-channel notification API and in-app feed service. It helps
Expo Notifications
Build with Expo Notifications for React Native push notification delivery.
Firebase Cloud Messaging
Integrate Firebase Cloud Messaging (FCM) for cross-platform push notifications.
Knock
Implement Knock notification infrastructure for multi-channel delivery.