Web Push
Encrypt and send browser notifications with VAPID.
Encrypt and send browser notifications with VAPID — the welcome ping, the “report ready” alert, or a silent data sync to a service worker.
The one rule
Create this transport under createPushSender and pass a browser
subscription — not an FCM device token.
Quick start
Generate VAPID keys
import { generateVapidKeys } from "sently/transports/webpush";
const { publicKey, privateKey } = await generateVapidKeys();
// Store privateKey in env / secrets manager. Use publicKey in the browser subscribe call.Configure the sender
import { createPushSender } from "sently/push";
import { WebPushTransport } from "sently/transports/webpush";
const push = createPushSender({
transport: new WebPushTransport({
vapidPublicKey: process.env.VAPID_PUBLIC_KEY!,
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY!,
subject: "mailto:you@example.com",
}),
});Send
await push.send({
subscription,
title: "Report ready",
body: "Your weekly report is ready to view.",
urgency: "high",
topic: "report-ready",
});Configuration
| Option | Type | Default or requirement |
|---|---|---|
vapidPublicKey | string | required — base64url uncompressed P-256 (65 bytes) |
vapidPrivateKey | string | required — base64url raw private key (32 bytes) |
subject | string | required — mailto:you@example.com or https://example.com/contact |
allowedEndpointHosts | string[] | optional — exact hostnames for private push relays |
Send options (Web Push)
| Field | Type | Notes |
|---|---|---|
subscription | PushSubscription | required — endpoint, keys.p256dh, keys.auth |
title / body | string | required together for a visible notification |
data | Record<string, unknown> | optional; required for silent / data-only |
icon / badge / image | string | Notification API URLs |
tag | string | replace an existing notification with the same tag |
actions | { action, title, icon? }[] | action buttons |
requireInteraction | boolean | keep open until the user interacts |
renotify | boolean | re-alert when replacing by tag |
ttl | number | seconds (default 2419200 / 28 days) |
urgency | "very-low" | "low" | "normal" | "high" | RFC 8030 Urgency header |
topic | string | RFC 8030 Topic — 1–32 printable ASCII; collapses pending messages |
silent | boolean | encrypt only data (no visible fields); requires data |
messageId | string | optional client id |
Features
Pick a branch. Channel send goes through push; key generation is imported from
sently/transports/webpush.
Visible notification via the channel sender.
await push.send({
subscription,
title: "Report ready",
body: "Your weekly report is ready to view.",
});Invalid urgency / topic, silent without data, or a partial visible
payload (title without body) throw WebPushError (provider: "webpush")
with status 400 before fetch.
Troubleshooting
No. Use the sently sender; provider-specific extras stay on the transport module.
VAPID subject must be a real mailto: address or https: URL. Values like @oke.local (no prefix) are rejected immediately as WebPushError so push services never return a confusing 403 later.
silent: true requires data. Without it the transport throws WebPushError with status 400.
topic must be 1–32 printable ASCII characters (no spaces). Invalid values throw before fetch.
Learn more
- Push channel — sender, hooks, plugins
- Push options — union fields for Web Push and FCM
- Web Push interoperability — browser subscription shape