Send Web Push messages with Deno
Lately, I've been working on a new webpush
package built from the ground up using only Web APIs. This make it possible
(theorically, at least) to send Web Push messages directly from your browser.
This blog post aims to explain what is the Web Push protocol, how it works (RFC 8291) and how to send Web Push messages using my library.
What is the Web Push protocol?
Web Push protocol is an intermediated protocol that allows an application to send messages to a user agent (a browser typically).
It is similar to Server-Sent Events (SSE) in the sense that messages are pushed to the user agent but it serves a different purpose. Web Push messages doesn't require websites to have an open tab as service workers can listen for push messages. It works in the background.
How does the Web Push protocol works?
Web Push protocol involve three actors:
- User-Agent: your website visitor browser typically
- Push Service: push server maintained and owned by Google, Mozilla or Apple depending on the browser
- Application Server: your server
Here's an overview of interactions between them:
+-------+ +--------------+ +-------------+
| UA | | Push Service | | Application |
+-------+ +--------------+ +-------------+
| | |
| Setup | |
|<====================>| |
| Provide Subscription |
|-------------------------------------------->|
| | |
: : :
| | Push Message |
| Push Message |<---------------------|
|<---------------------| |
| | |
An intermediate push service is required for multiple reasons.
First, it reduces bandwidth and battery usage as user agents maintains only a single connection for all websites instead of one per website.
It also improves scalability and reliability as major browsers' push services are designed to handle millions of users. Because push messages must be retained if user agent is offline, building a push service requires a lot of engineering, a resilient and redundant infrastructure
Finally, building, deploying, and maintaining a custom push service is often too complex and resource-intensive for small web companies. This would give larger corporations an unfair competitive advantage, as they would have the necessary resources to develop and refine their own push services.
If you're a privacy concerned user like me, seeing an intermediary service
receiving all messages raise red flags. To address this concern, Web Push
messages are secured through HTTP Encrypted Content-Encoding (see my
http-ece
package), ensuring that
sensitive information remains protected and unreadable to any third-party
services in transit.
Setup
You may have noticed that setup arrow is different than others in ASCII graph
above. This is because setup phase is implementation dependent. All major
browsers implements the javascript
Push API in a
different way. A
PushManager.subscribe()
method that returns a standard
PushSubscription
is exposed.
Subscriptions always contains a unique URL endpoint associated with the push subscription and a public key used to encrypt messages.
When creating a subscription, an optional applicationServerKey
may be provided
to identify application server pushing messages. This is the Voluntary
Application Server Identification (VAPID) authentication method
(RFC 8292). VAPID keys are used
to mitigate DDOS attacks on push services. Also adding authentication between
application server and push service reduce risks of leaking subscription
endpoint. For these reasons, they are mandatory in Firefox.
Provide Subscription
Second step is to send the subscription to Application server so it can start sending messages.
Application server will typically store subscription in a database for later reuse.
Push message
Finally, to push a message, application server sends an encrypted HTTP request
with a vapid
authentication scheme iff applicationServerKey
was provided to
create subscription.
If user agent is online when the message is received by push service, it is forwarded. Otherwise, it is stored until user agent become online or message expire.
When user agent receive a message, it executes the push
event handler that is
mostly used to display a notification and that's it.
Setting up an application server using webpush
First you must generate VAPID keys as some browsers make them mandatory:
$ deno run https://raw.githubusercontent.com/negrel/webpush/master/cmd/generate-vapid-keys.ts
Copy output and save it to a file, you don't need to generate VAPID keys again.
In your application server code, you can load them as follow:
import * as webpush from "jsr:@negrel/webpush";
// Read generated VAPID file.
const vapidKeysJson = Deno.readTextFileSync("./path/to/vapid.json");
// Import VAPID keys.
webpush.importVapidKeys(JSON.parse(vapidKeysJson));
Then, you will need to create an ApplicationServer
object instance.
// adminEmail is used by Push services maintainer to contact you in case there
// is problem with your application server.
const adminEmail = "john@example.com";
// Create an application server object.
const appServer = await webpush.ApplicationServer.new({
contactInformation: "mailto:" + adminEmail,
vapidKeys,
});
Then to send push messages, simply create a PushSubscriber
and call its
pushMessage()
/pushTextMessage()
method as follow:
const subsribers = [];
// HTTP handler for user agent sending their subscription.
function subscribeHandler(req) {
// Extract subscription send by user agent.
const subscription = await req.json();
// Store subscription in db.
// ...
// Create a subscriber object.
const sub = appServer.subscribe(subscription);
// Store subscriber in memory.
subscribers.push(sub);
}
// Helper method to send message to all subscribers.
function broadcastMessage(msg) {
for (const sub of subscribes) {
sub.pushTextMessage(msg, {});
}
}
That's it, your sending push messages to your subscribers!
webpush
repository contains an interactive
example with similar code that you can run locally. It also contains client side
javascript code so be sure to check it out!