You've probably seen web notifications before. YouTube shows them when it goes to a new song, Facebook pings them when a new message comes in, scammy websites ask for permissions and you say no. The usual.
You can fire those notifications from anywhere inside your JavaScript.
const notification = new Notification(title, {
body: body,
icon: iconURL,
})
That creates a new browser notification and shows it to the user. Since Chrome 60-something on a Mac, they're integrated with native notifications. It's great.
You can close the browser notification with notification.close
, and you can listen for click events with notification.onclick = () => ...
. Most often you'd want to use that click event to focus on your browser tab.
To make this work however, the user must keep your webapp open and you must keep your fingers crossed the browser hasn't throttled your JavaScript too much for inactivity. Browsers do that, you know.
But did you know you can also add real push notifications to your webapp? The kind that come from a server and work even with no open tabs? Oh yes, just like a mobile app.
I had no idea this has been part of Chrome since version 40-something. Firefox supports it, too. Safari and Internet Explorer, no.
It is not a part of the JavaScript standard yet. MDN lists web push notification support as experimental.
Here's what you'll need to make it work 👇
- a service worker
- some code to register the service worker
- some code to ask for notification permissions
- bit of code to subscribe to web push notifications
- a server to trigger the push notification
Service worker that listens for push events
You can think of service workers as a piece of JavaScript that lives in your user's browser and does stuff. They can do stuff even when your site isn't loaded, or even open.
Most commonly they're used for caching in what's called progressive web apps – PWA. They intercept requests and serve code from a local cache. Even if the browser is offline.
But none of that right now. We're using them for push notifications.
A service worker will live in our user's browser and listen for push
events. The browser gets these events from a so-called push service, triggers the service worker, and our service worker does whatever.
For security reasons, we can't control which push service the browser uses. The browser tells us instead. You'll see how in the section about subscribing.
For privacy reasons, we also have to accept a sort of gentleman's agreement with the browser where we promise to always show a notification when a push event comes in.
So… a service worker is just a JavaScript file. Nothing fancy going on. You use self
to refer to the global scope because there's no window
and no document
.
// service-worker.js
function showNotification(event) {
return new Promise(resolve => {
const { body, title, tag } = JSON.parse(event.data.text());
self.registration
.getNotifications({ tag })
.then(existingNotifications => { // close? ignore? })
.then(() => {
const icon = `/path/to/icon`;
return self.registration
.showNotification(title, { body, tag, icon })
})
.then(resolve)
})
}
self.addEventListener("push", event => {
event.waitUntil(
showNotification(event)
);
}
});
self.addEventListener("notificationclick", event => {
event.waitUntil(clients.openWindow("/"));
});
That's all the code that goes into our service worker.
In showNotification
, we return a Promise
that resolves once we're done showing our notification.
We use JSON.parse
to unpack notification parameters using the convention of putting JSON into the message
portion of our push notification.
Before showing our push notification with self.registration.showNotification
, we can clear any existing notifications with the same tag
using self.registration.getNotifications
. This gives us a list of notifications that we can .close
.
I've noticed (at least on a Mac) that subsequent notifications with the same tag
sometimes don't show up unless you close the previous ones.
We use self.addEventListener
to listen for both push
and onclick
events. In the push handler, we call showNotification
to show our web push notification, and in notificationclick
we open our site.
That's the service worker 😄
Curated Fullstack Web Essays
Get a series of curated essays on Fullstack Web development. Lessons and insights from building software for production. No bullshit.
Register the service worker
Registering said service worker happens in our regular JavaScript code. Usually in the main index.js
entry point because there can be only one service worker for an entire domain.
function registerServiceWorker() {
navigator.serviceWorker
.register("/service-worker.js")
.then((registration) => {
console.log("ServiceWorker registered with scope:", registration.scope)
})
.catch((e) => console.error("ServiceWorker failed:", e))
}
if (navigator && navigator.serviceWorker) {
registerServiceWorker()
}
We call navigator.serviceWorker.registe
with the URL of our service worker and wait for the promise to resolve. If all goes well, we print a success message, otherwise we print an error.
This is not a piece of code you'll have to write often. Once per project at most. Many just find it online and copypasta when they need it, I think.
One caveat to keep in mind is that service workers are limited to the scope they're served from. You can't serve that file from a CDN, or from a static.domain.com
, or even domain.com/static/
.
This is a security precaution.
My solution was to use the Webpack sw-loader
and configure it to compile this particular file into a different location and with a different publicPath than all other JavaScript, which goes into CDNs and has fingerprinting and stuff.
That part was tricky, but it's very specific to every webapp, so it’s not a good candidate for this article :)
Ask for web notification permissions
If you've ever used web notifications before, you already know this part: You have to ask the user for permission.
Here's how that goes
const permission = Notification.requestPermission()
if (permission !== "granted") {
// no notifications
} else {
// yay notifications
}
Yep, that's it.
Running Notification.requestPermission
pops up a little dialog for our user asking them to grant us web notification permissions. If they do, the permission is set to granted
. Other options include denied
, and I think it stays null if they ignore us.
You can check permission status at any time with Notification.permission
.
Subscribe to web push notifications
Here comes the interesting part: subscribing to web push notifications. This is where we ask the browser "Hey, which push service do you wanna use?"
The process has 2 to 3 steps depending on how you count 👇
- Create VAPID keys for our server, one-time
- Ask browser to subscribe
- Save subscription info for our user
VAPID keys are a way for our server to identify itself with the push service. That way our user's browser can be sure that push notifications are coming from us and not from some random spammer who got in the way.
I used the Webpush
gem to generate these keys, there's a web-push
package for node as well.
# One-time, on the server
vapid_key = Webpush.generate_key
# Save these in your application server settings
vapid_key.public_key
vapid_key.private_key
You will have to send the public_key
to your frontend somehow because you'll need it to subscribe to web push notifications. The subscription itself happens like this:
function subscribeToPushNotifications(registration) {
return registration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: window.vapidPublicKey,
})
.then((pushSubscription) => {
console.log(
"Received PushSubscription:",
JSON.stringify(pushSubscription)
)
return pushSubscription
})
}
We call registration.pushManager.subscribe
to subscribe with our applicationServerKey
. That's the vapid.public_key
. And as I mentioned earlier, we promise to always show a notification with userVisibleOnly: true
.
Not a single browser currently supports userVisibleOnly: false
because it could lead to privacy issues. Think sending a push and having your service worker send back detailed GPS location for your user. No bueno.
That pushSubscription
will have a bunch of data inside. Everything from which API endpoint to use when sending notifications to info about how to authenticate with the service.
The browser is in full control.
You should save that info on the backend and associate it with your user. As far as I can tell, it's unique for every subscription request and definitely for every browser.
The tricky part here is when the same user is using multiple browsers and devices. If you want to send push notifications to all of them, you're going to have to keep multiple copies of this info.
Trigger a web push notification
To trigger a web push notification from your server, I suggest using a library. Something like Webpush for Ruby or web-push for node. I'm sure libraries exist for other languages as well.
Here's how you'd send a notification in Ruby/Rails:
def send_web_push_notification(user_id)
subscription = User.find(user_id).web_push_subscription
message = {
title: "You have a message!",
body: "This is the message body",
tag: "new-message"
}
unless subscription.nil?
Webpush.payload_send(
message: JSON.generate(message),
endpoint: subscription["endpoint"],
p256dh: subscription["keys"]["p256dh"],
auth: subscription["keys"]["auth"],
ttl: 15,
vapid: {
subject: 'mailto:admin@example.com',
public_key: Rails.application.config.webpush_keys[:public_key],
private_key: Rails.application.config.webpush_keys[:private_key]
}
)
end
end
We use a database JSON field called web_push_subscription
to save the pushSubscription
info on our users.
If that field has info, we can use Webpush
to send a notification to the API. The API then sends it out to our service worker, which then shows a notification.
Fin
You should now be able to add web push notifications to your webapp. I left out some details around setting up Webpack to serve service worker code correctly, but we can get into that some other day.
If you do decide to add push notifications to your webapp, please use them responsibly. Remember what happened to mobile app notifications and what a mess that has become.
Wouldn't want to train users to automatically deny notification permissions now would we :)
Continue reading about How to add real web push notifications to your webapp
Semantically similar articles hand-picked by GPT-4
- How serverless beats servers
- Learning WebRTC peer-to-peer communication, part 2 – connecting 2 browsers on different devices
- Go full-stack in 5min with your first cloud function
- Modern backend is a JavaScript function
- How to waste hours of life with fetch() and a bit of brainfart
Want to become a Fullstack Web expert?
Learning from tutorials is great! You follow some steps, learn a smol lesson, and feel like you got this. Then you go into an interview, get a question from the boss, or encounter a new situation and o-oh.
Shit, how does this work again? 😅
That's the problem with tutorials. They're not how the world works. Real software is a mess. A best-effort pile of duct tape and chewing gum. You need deep understanding, not recipes.
Leave your email and get the Fullstack Web Essays series - a series of curated essays and experiments on modern Fullstack Web development. Lessons learned from practice building production software.
Curated Fullstack Web Essays
Get a series of curated essays on Fullstack Web development. Lessons and insights from building software for production. No bullshit.
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️