better-push
Delivery channels

Web Push setup

VAPID keys, the service worker, and the HTTPS requirement explained.

Web push has three moving parts you have to get right: VAPID keys, a service worker served from your origin, and HTTPS. This page covers each.

Registration UI

The styled copy-in toggle exposes the browser states users need to understand. It uses fixture state here, so it never opens a permission prompt or calls an API.

Interactive browser push toggle covering available, busy, subscribed, denied, unsupported, and error states.

VAPID keys

VAPID (Voluntary Application Server Identification, RFC 8292) is how a push service knows your server is allowed to send to a subscription. You need one key pair for your whole application.

import { generateVapidKeys } from "@better-push/core/providers/web-push";

const { publicKey, privateKey } = generateVapidKeys();
  • The private key stays on your server and is passed to webPush({ vapid }).
  • The public key is passed to webPush({ vapid }) and handed to the browser client as applicationServerKey.
  • The subject must be a mailto: or https: URL - a contact the push service can reach if there is a problem.
webPush({
  vapid: {
    subject: "mailto:you@example.com",
    publicKey: process.env.VAPID_PUBLIC_KEY!,
    privateKey: process.env.VAPID_PRIVATE_KEY!,
  },
  ttl: 86400, // optional, seconds a push service holds a message; default 1 day
});

Rotating VAPID keys invalidates every existing subscription, so treat them as long-lived secrets.

The service worker

A push notification is delivered to a service worker, not to a page - that is what lets a notification arrive when your site is not open. Service workers cannot import npm modules without a build step, so better-push ships the worker as a static file for you to copy to public/sw.js. The exact source is also exported:

import { SERVICE_WORKER_SOURCE } from "@better-push/core/client/sw";

The worker handles two events:

  • push - reads the JSON payload and calls showNotification(title, { body, data }).
  • notificationclick - focuses an existing window if there is one, else opens data.url (or /).

The payload your worker receives is the JSON string { notificationId, title, body, data } produced by push.notify().

The HTTPS requirement

Service workers and the Push API require a secure context. The one exception is http://localhost, which browsers treat as secure - so local development needs no certificates.

Everywhere else, including a phone pointed at your machine's LAN IP, you need real HTTPS. This is why testing on devices means deploying to an HTTPS URL. See Testing On Devices.

How sending maps to the push protocol

When you call push.notify(), the web push provider:

  1. Parses each device's stored subscription JSON.
  2. Encrypts the payload per RFC 8291 and signs the request with your VAPID keys (via the mature web-push package - better-push does not reimplement the crypto).
  3. Sends to each subscription's endpoint with bounded concurrency.
  4. Maps the push service's response: 404/410 mean the subscription is gone, so the device is disabled; 413, 429, and other errors are recorded on the delivery row without disabling the device.

On this page