Tous les articles

·2 min de lecture

Web push notifications and their disposable subscriptions

A Web Push subscription isn't an account, the browser issues it and disposes of it. In Ramedex the phone sends its subscription back to the server on every open, and the old one gets overwritten. Plus a server-side surprise.

In Ramedex, you capture the real trains that pass near you. Push notifications exist to warn that a rare train is on its way. A message like that is most likely to arrive long after the first visit, on some morning when the player hasn't opened the game in a while. In between, their push subscription has had all the time it needed to disappear or change keys, and none of that is visible from the server.

The subscription belongs to the browser

A Web Push subscription is not an account. It's an endpoint, a URL the browser's push service keeps for that one device, together with two keys to encrypt the payload. The browser issues it and the browser disposes of it. It can expire it, generate a new one, or let it die when the player clears the site's data. The first idea that comes to mind is to store it when the player accepts notifications, then treat it as a done deal. That holds on launch day. Then it quietly degrades.

Send it back every time the game opens

Ramedex treats it as disposable. Each time the game opens, the phone sends its subscription back to the server. If it no longer has one, it subscribes. If the keys have changed, it starts from a fresh subscription and the old one gets overwritten. The subscription stored server-side is therefore at most as old as the last open.

Here is the browser side, simplified.

const registration = await navigator.serviceWorker.ready;
let subscription = await registration.pushManager.getSubscription();

if (!subscription) {
  subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: vapidKey, // the server's public key, as a Uint8Array
  });
}

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

There is nothing conditional in there. The call goes out on every open, even if the subscription hasn't changed since yesterday. That choice matches how the game is used. A rare-train alert only has value on a phone that's still in use. And a phone that's still in use will open Ramedex again sooner or later, and resend its subscription.

The surprise was on the server side

Without the PHP gmp and bcmath extensions in the Docker image, sending failed. The library that sends the notifications depends on them, and the official PHP image doesn't ship with them. Once both extensions were added, the alerts went through.

What I take away from this is a cost comparison. Resubscribing on every open is a few lines of browser code. Managing dead subscriptions means listening to the errors the endpoints return, cleaning up, retrying, and accepting that part of the sends goes nowhere in the meantime. I much prefer the first option. What I'm left with are the phones that never open the game again. Their stale subscription just sits on the server, and nothing flags it until the first send fails.