I'm guessing that in the real world, most users don't unsubscribe and re-subscribe very often. However, Push subscriptions getting stale can be a problem. And users with more than one device can get notified on the wrong device if your server is using the most recent Push subscription and the user has meanwhile returned to a device with a subscription which is older, but still valid.
One way to deal with this, which I have considered and dismissed, would be to re-subscribe the user every time they fire up a client. However, I could find no information from the Apple/Google/etc (the big browser push service providers) as to whether there are any limits on this or whether it's a bad practice. Don't want to get black-listed.... [BTW, Apple gives errors to push requests as JSON in the response content, whereas Google sends plain text. Broke my server code with Chrome client after writing it while testing on Safari.]
So, rather than simply inserting the latest subscription into, say, a column in a users db table, perhaps one should insert it into a new table with a primary key consisting of user_id /and/ browser_id. I'll show an example of what browser_id might be below. Then, when the user comes from a particular browser on a particular device, look up the appropriate subscription and fire away at the endpoint.
[Another option would be to store the complete subscription JSON in a cookie or localStorage and send it to your server very often, but a few hundred extra bytes on every HTTP request seems like a waste. Thoughts anyone?]
Anyway, here's my take on making a smaller browser_id and using it along with user_id to look up the subscription JSON on the server side (the comment is a little redundant; you may just want to look at the code):
/*
This is to help the server use the right web Push API subscription if the user switches
between different browsers; e.g. say a user is on browser A and the server caches a push
subscription for them, and then they go to browser B and we cache a new subscription for
them, and then they go back to browser A and we don't cache a new subscription for them
because browser A already has a subscription going because the service worker is still
running from their previous session; then the server wants to send them a notification
and it uses the browser B subscription instead of the browser A one; clearly the
subscriptions need to be cached per user, per browser -- but how to identify each browser
uniquely? Upon first visiting the site, we'll create a browser_id that combines the
users initial IP address with a timestamp; if the visitor is later issued a user_id,
we will store any Push subscriptions in a db table with primary key of user_id and
browser_id. Sample browser_id: 10.0.0.255-1680711958431
*/
async function set_browser_id() {
let browser_id = Cookies.get('browser_id') || localStorage.browser_id;
if ( ! browser_id) {
/*
const response = await fetch("https://api.ipify.org?format=json");
const jsonData = await response.json();
const ip_addr = jsonData.ip;
*/
const timestamp = Date.now();
browser_id = `${ip_addr}-${timestamp}`; // server set JS const ip_addr earlier
console.log(`NEW browser_id: ${browser_id}`);
}
else { console.log(`EXISTING browser_id: ${browser_id}`); }
// set whether new or existing to keep it from ever expiring
Cookies.set('browser_id', browser_id,
{ path: '/', domain: `.${hostname}`, expires: 365, secure: true });
localStorage.browser_id = browser_id; // backup in persistent storage
}
window.onload = set_browser_id; // do it after everything else
The browser_id should be pretty persistent given the localStorage backup of the cookie (and might be useful for analytical purposes). Now your server can get a short little browser_id cookie with every request and can use it to look up the longer Push subscription JSON. In fact, you could probably skip the user_id in the db table since the chance of a browser_id namespace collision is infinitesimal. Of course, whenever the user generates a new subscription, you'll need the client to send that over so the server can update the db table.
I'd be curious what others think about this plan; I'm implementing it presently. And also about any security concerns with passing the complete subscription JSON around in cookies repeatedly -- your private VAPID key is needed to send a notification, right? But....
p.s. I'm on about this now since Apple /finally/ turned on the Push API with iOS 16.4 -- yay! About time.