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.

see also