N

Send Web Push messages with Deno

#opensource
#deno
#web

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!