demo · v138
Server-sync flow
Walk the three-actor dance: SW → browser push service → your backend. Trigger a key rotation (the case 138 added the event for) and watch the backend learn the new endpoint without the user reopening the page.
Browser capability check
Service Worker
no subscription
Push service (FCM/Mozilla)
no endpoint
Your backend (subscription table)
empty
the SW listener
self.addEventListener("pushsubscriptionchange", async (event) => {
const { oldSubscription, newSubscription } = event;
// Pre-138: this event only fired on revoked/unsubscribed.
// 138+: also fires on silent resubscription (key rotation, GCM → FCM migration, etc.)
if (newSubscription) {
await fetch("/api/push/replace", {
method: "POST",
body: JSON.stringify({
oldEndpoint: oldSubscription?.endpoint,
newEndpoint: newSubscription.endpoint,
keys: newSubscription.toJSON().keys,
}),
});
} else {
await fetch("/api/push/remove", { method: "POST", body: JSON.stringify({ endpoint: oldSubscription.endpoint }) });
}
});
The missing event was a long-standing bug-source: a browser would rotate VAPID keys (e.g. when migrating from GCM to FCM, or on cleanup), the old endpoint silently stopped accepting pushes, and the backend kept sending to the stale URL. The user would think they’d unsubscribed even though the subscription was alive. 138 plugs that gap.