# Web Push and PushManager.subscribe()

> How PushManager.subscribe() creates a VAPID-keyed PushSubscription, what endpoint, p256dh, and auth are for, and how push events reach the service worker.

import Figure from '@components/Figure.astro';
import pushFlowDiagram from '@assets/diagrams/push-flow.svg';

`registration.pushManager.subscribe()` asks the browser's push service for a `PushSubscription`
whose `endpoint` your server can POST encrypted messages to; the browser wakes the service
worker with a `push` event for each message even when no page is open, and the worker shows a
notification. Together with the Notifications API this is Web Push, the only standard way for a
server to reach a PWA user who has closed the app.

<Figure src={pushFlowDiagram} alt="Sequence diagram of Web Push across the web page, service worker, push service and application server: the page requests notification permission, calls pushManager.subscribe() with the VAPID public key, receives a PushSubscription and posts it to the server. The server sends a VAPID-signed, encrypted request to the endpoint, the push service delivers it, the browser fires the push event in the service worker, which calls showNotification(). A tap fires notificationclick, and pushsubscriptionchange triggers a resubscribe." caption="Web Push from subscription to notification: who talks to whom, and which event fires where." />

## Syntax

```js
registration.pushManager.subscribe(options)
registration.pushManager.getSubscription()
registration.pushManager.permissionState(options)
subscription.toJSON()
subscription.unsubscribe()
```

`subscribe()` resolves with a `PushSubscription`, or with the existing one when the origin is
already subscribed with the same key. `getSubscription()` resolves with the current subscription
or `null`. All three are available from a window and from the service worker's own
`self.registration`. Support: Chrome 42, Firefox 44, Edge 17, Safari 16 on macOS Ventura, Safari
16.4 on iOS for Home Screen web apps (BCD `api.PushManager.subscribe`). Firefox 72+ requires
`subscribe()` to run inside a user gesture; Chromium requires `applicationServerKey`.

## Parameters

| Parameter | Type | Meaning |
|---|---|---|
| `options.userVisibleOnly` | boolean | Promise that every push results in a visible notification. Chromium and WebKit reject `false` with `NotAllowedError`; WebKit also revokes a subscription whose `push` handler fails to show one ([Meet Web Push](https://webkit.org/blog/12945/meet-web-push/), webkit.org). Firefox accepts `false` and applies a quota to silent pushes instead. |
| `options.applicationServerKey` | `BufferSource` or base64url string | Your VAPID public key, an uncompressed P-256 point of 65 bytes. The server signs each send with the matching private key ([RFC 8292](https://datatracker.ietf.org/doc/html/rfc8292), ietf.org). Required in Chromium; a subscription without it in Firefox cannot be migrated to VAPID later without unsubscribing. |

The returned `PushSubscription` serialises with `toJSON()` to `{ endpoint, expirationTime, keys:
{ p256dh, auth } }`. `endpoint` is a capability URL on the browser vendor's push service
(`https://fcm.googleapis.com/fcm/send/…` for Chrome, a `*.push.apple.com` host for Safari);
anyone holding it can send to the device, so it is transmitted and stored like a credential.
`p256dh` and `auth` are the client's public key and authentication secret for RFC 8291 payload
encryption. `expirationTime` is `null` in every current engine.

## Exceptions

| Exception | When |
|---|---|
| `NotAllowedError` `DOMException` | Notification permission is `"denied"`, or the user dismissed the prompt that `subscribe()` triggered; `userVisibleOnly` is `false` in Chromium or WebKit; the call is outside a user gesture in Firefox 72+; the service worker's scope is not a secure context. |
| `InvalidStateError` `DOMException` | The registration has no active worker, or a subscription already exists with a different `applicationServerKey`. Unsubscribe before changing keys. |
| `InvalidAccessError` `DOMException` | `applicationServerKey` is not a valid P-256 public key (wrong length or not an uncompressed point). |
| `InvalidCharacterError` `DOMException` | The base64url string form of `applicationServerKey` fails to decode. |
| `AbortError` `DOMException` | The push service could not create the subscription, typically because the device is offline or the service is unreachable. Retry later. |

Server-side failures are HTTP statuses from the push service, not exceptions: `404` and `410`
mean the subscription is gone and must be deleted; `413` means the payload exceeds the 4 KB
limit; `429` means the service is rate-limiting the application server.

## Examples

The page subscribes, the service worker receives, and a third snippet checks what already exists before doing either.

### Subscribing from a click and posting the subscription to the server

Permission and subscription happen in one gesture. The key arrives from the server as a
base64url string, which the specification allows `subscribe()` to take directly; the
`urlBase64ToUint8Array()` conversion seen in older tutorials is only needed for engines that
predate that change.

```js
button.addEventListener('click', async () => {
  const registration = await navigator.serviceWorker.ready;
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') return;
  const { publicKey } = await (await fetch('/api/push/key')).json();
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: publicKey,
  });
  await fetch('/api/push/subscriptions', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(subscription.toJSON()),
  });
});
```

Store the subscription keyed by user and device, and delete it when a send returns `404` or
`410`; a stale endpoint that keeps being pushed to is the most common cause of silent delivery
failure.

### Receiving the message and resubscribing when the endpoint changes

The `push` handler must end in `showNotification()`. `pushsubscriptionchange` fires when the push
service rotates the endpoint; the worker resubscribes with the same key and tells the server.

```js
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {};
  event.waitUntil(
    self.registration.showNotification(data.title ?? 'Update', {
      body: data.body ?? '',
      data: { url: data.url ?? '/' },
    })
  );
});

self.addEventListener('pushsubscriptionchange', (event) => {
  event.waitUntil((async () => {
    const key = event.oldSubscription?.options.applicationServerKey;
    const subscription = await self.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });
    await fetch('/api/push/subscriptions', { method: 'POST', body: JSON.stringify(subscription.toJSON()) });
  })());
});
```

`pushsubscriptionchange` fires in Firefox 44 (without `oldSubscription`, bug 1497429), Safari 16,
and Chrome 138 (BCD `api.ServiceWorkerGlobalScope.pushsubscriptionchange_event`); before Chrome 138
a server can only detect rotation by the `410` it receives.

### Detecting support and reusing an existing subscription

Check for `PushManager` on the registration, then prefer `getSubscription()` so that a reload
does not create a second subscription. The fallback is polling or in-app messages.

```js
async function currentSubscription() {
  if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
    return null; // no push: poll for updates while the app is open
  }
  const registration = await navigator.serviceWorker.getRegistration();
  if (!registration?.active) return null;
  return registration.pushManager.getSubscription(); // existing subscription or null
}
```

A `null` here with permission `"granted"` means the user has not subscribed yet, which is the
moment to show the enable button from the first example.

:::observed
`subscription.toJSON()` in Chrome returns an object whose `endpoint` begins with
`https://fcm.googleapis.com/fcm/send/` and whose `expirationTime` is `null` ([Push notifications
overview](https://web.dev/articles/push-notifications-overview), web.dev); in Safari the endpoint
host is under `push.apple.com`, which WebKit asks server operators to allow-list ([Meet Web
Push](https://webkit.org/blog/12945/meet-web-push/), webkit.org). Chrome DevTools, Application >
Service workers, has a **Push** text field and button beside the registration that dispatches a
`push` event with the typed payload to the worker without any server, which is how the handler
above is tested before VAPID keys exist.
:::

## Try it

The companion demo at [/demo/#push](/demo/#push) calls `Notification.requestPermission()` and then
`pushManager.subscribe()` with a VAPID public key generated for the demo, and prints the resulting
`PushSubscription` JSON. There is no server behind it: nothing is sent and no message arrives,
which makes it a safe place to see the permission flow and the subscription shape.

## See also

- [Push API: subscribe() method](https://www.w3.org/TR/push-api/#dom-pushmanager-subscribe) (w3.org)
- [RFC 8292: Voluntary Application Server Identification (VAPID) for Web Push](https://datatracker.ietf.org/doc/html/rfc8292) (ietf.org)
- [Push notifications overview](https://web.dev/articles/push-notifications-overview) (web.dev)
- [Notifications API](/reference/notifications/notifications-api/)
- [Web Push on iOS and iPadOS](/reference/notifications/ios-safari-push/)
- [Notification.requestPermission()](/reference/notifications/permissions/)