howdy/notifications/web_push

Web Push: notifications from the browser itself, which reach people whose tabs are closed.

import howdy/notifications/web_push

let assert Ok(public) = env.get("VAPID_PUBLIC_KEY")
let assert Ok(private) = env.get("VAPID_PRIVATE_KEY")
let assert Ok(keys) = web_push.keys(public:, private:)
let push = web_push.new(notifier, keys, contact: "mailto:ops@example.com")
let notifier = notifications.with_push(notifier, web_push.deliver(push))

howdy.new()
|> howdy.controller(web_push.routes(push, at: "/notifications/push", identify:))

Make the keys once with gleam run -m howdy/notifications/web_push and keep them in the environment: browsers subscribe with the public key, and every subscription stops working if it changes.

A user turns push on in each browser they want it in. howdy/ui/bell offers that in its panel when given the routes’ path with bell.push: the browser registers the service worker served by routes, subscribes with the push service its maker runs (Google’s for Chrome, Mozilla’s for Firefox, Apple’s for Safari), and the app keeps the subscription. A notification then goes to each of the user’s subscriptions, encrypted so that only that browser can read it (RFC 8291), and signed with the app’s keys (RFC 8292). Clicking it opens its link.

The app sends to whatever address a subscription gives, so only the push services of the major browsers are accepted, by default; see allow_endpoints. Subscriptions the push service says have gone are deleted.

Types

The app’s VAPID key pair, which identifies it to push services.

pub opaque type Keys

Where and how to reach one browser, as its PushSubscription gives it.

pub type Subscription {
  Subscription(endpoint: String, p256dh: String, auth: String)
}

Constructors

  • Subscription(endpoint: String, p256dh: String, auth: String)
pub opaque type WebPush

Values

pub fn allow_endpoints(
  push: WebPush,
  allow: fn(uri.Uri) -> Bool,
) -> WebPush

Accept subscriptions whose endpoint allow accepts, as well as or instead of the major browsers’ push services. Endpoints are always https.

pub fn deliver(
  push: WebPush,
) -> fn(notifications.Notification) -> notifications.Outcome

Deliver a notification to every browser its recipient turned push on in, for notifications.with_push. A recipient with none is done at once. Subscriptions the push service says have gone are deleted.

If any browser’s push service asks to be tried later, the whole notification is tried again, so the others may get it twice; it is sent with its ID as the tag, so the browser shows it once.

pub fn generate_keys() -> Keys

A new key pair. Make one once and keep it; see the module docs.

pub fn keys(
  public public: String,
  private private: String,
) -> Result(Keys, Nil)

Keys as generate_keys makes them and public_key and private_key write them: base64url, the public key an uncompressed P-256 point and the private key its 32 byte scalar. An error when they are not, or do not belong together.

pub fn known_push_service(endpoint: uri.Uri) -> Bool

Whether an endpoint belongs to the push service of Chrome, Edge, Firefox or Safari. Chromium builds other than Google’s own, such as Linux distributions’, subscribe through jmt17.google.com.

pub fn main() -> Nil

Print a new key pair, as environment variables: gleam run -m howdy/notifications/web_push.

pub fn new(
  notifier: notifications.Notifier,
  keys: Keys,
  contact contact: String,
) -> WebPush

Push for notifier’s users. contact is a mailto: or https: URL a push service can use to reach whoever runs the app. Panics on any other: Apple’s push service refuses every notification signed without one. Make it a real address, not one on localhost, which Apple also refuses.

pub fn private_key(keys: Keys) -> String

The private key, base64url. Keep it secret.

pub fn public_key(keys: Keys) -> String

The public key, base64url, which browsers subscribe with.

pub fn routes(
  push: WebPush,
  at prefix: String,
  identify identify: fn(context.Context(Nil)) -> Result(
    String,
    service.Error,
  ),
) -> controller.Builder(Nil)

What browsers need to turn push on and off, for bell.push or a page of your own. identify names the signed-in user, as for notifications.routes, and must refuse cross-site writes.

  • GET /worker.js is the service worker, which shows each notification and opens its link when it is clicked.
  • GET /key gives {"key": "..."}, the public key to subscribe with.
  • POST /subscriptions keeps the JSON of a browser’s subscription for the user.
  • DELETE /subscriptions forgets one, given {"endpoint": "..."}.
pub fn send(
  push: WebPush,
  subscription: Subscription,
  message: String,
) -> notifications.Outcome

Encrypt and send one message to one browser.

pub fn service_worker() -> String

The service worker’s source. It shows each push as a notification, tagged with the notification’s ID, and on a click focuses a tab already at its link or opens one.

pub fn subscribe(
  push: WebPush,
  recipient: String,
  subscription: Subscription,
) -> Result(Nil, service.Error)

Keep a browser’s subscription for recipient. A subscription is one browser, so one already kept for someone else, after they signed out on a shared computer, moves to recipient.

pub fn subscription_decoder() -> decode.Decoder(Subscription)

The JSON a browser’s subscription.toJSON() gives.

pub fn subscriptions(
  push: WebPush,
  recipient: String,
) -> Result(List(Subscription), service.Error)

Every browser recipient has turned push on in.

pub fn time_to_live(push: WebPush, seconds: Int) -> WebPush

How long a push service keeps a notification for a browser that is offline, in seconds. Defaults to a day.

pub fn unsubscribe(
  push: WebPush,
  recipient: String,
  endpoint: String,
) -> Result(Nil, service.Error)

Forget a browser’s subscription, if it is recipient’s.

✨ Search Document