JavaScript Push API Table
| Piece | What it does | Field note |
|---|---|---|
PushManager | The entry | lives on the service worker registration - push is delivered to the worker, never the page |
subscribe({applicationServerKey}) | The VAPID gate | your server P-256 public key as raw bytes; subscriptions accept only messages signed by its twin |
PushSubscription | The handle | endpoint (a bearer capability URL) + keys (p256dh, auth) - store server-side, never leak client-side |
push event | The wake-up | worker-side listener; e.data is binary (json/text/arrayBuffer) and optional - wrap in e.waitUntil |
showNotification | The law | userVisibleOnly is enforced: every push must surface a notification or the browser revokes the channel |
pushsubscriptionchange | The renewal | endpoints rotate silently (updates, permission resets) - re-subscribe and re-enroll or die quietly |
getSubscription / unsubscribe | The lifecycle | reuse before re-subscribing; unsubscribe() on logout so dead endpoints stop burning quota |
support matrix | The platform truth | HTTPS + SW everywhere; iOS 16.4+ only for installed Home Screen apps - the install prompt is the push funnel there |
The Push API is the web's server-to-user channel: your server wakes a service worker on the user's device even when the site is closed - the SW receives a push event, shows a notification, and can sync data. The flow has three actors: the page subscribes via registration.pushManager.subscribe(), your server stores the resulting PushSubscription, and later POSTs encrypted payloads to its endpoint; the browser routes the message to the service worker, whose 'push' listener decides what the user sees.
Bottom line: push is a chain of custody, and every link is cryptographic or legal, not incidental. The applicationServerKey (a VAPID P-256 public key) is your server's identity - subscriptions only accept messages signed with the matching private key, so key rotation has a migration story (pushsubscriptionchange) or has a broken channel. The subscription endpoint is a capability URL - unique per browser and origin, and effectively a bearer token: whoever holds it can push. Store subscriptions server-side; never leak them.
The web's push has one law and one weakness. The law: userVisibleOnly - every push must show a notification (Chrome enforces it at subscribe time and revokes silent channels), because invisible push is a spyware pattern; silent data sync belongs to Background Sync. The weakness: subscriptions expire quietly - browsers rotate endpoints, permissions reset, and if you ignore pushsubscriptionchange your channel dies without an error anywhere. Design the renewal, or design for silence.
How to use
- Subscribe with identity: const sub = await reg.pushManager.subscribe({userVisibleOnly: true, applicationServerKey: urlB64ToUint8Array(PUBLIC_VAPID_KEY)}); - the key is your server's public VAPID key as raw P-256 bytes; getSubscription() first to reuse an existing one instead of stacking subscriptions.
- Enroll the subscription server-side: send sub.endpoint plus sub.toJSON().keys (p256dh and auth) to your backend and store them keyed by user - the endpoint is the address, the keys are what your server encrypts each payload to.
- Push from the server with a library: sign the JWT with your VAPID private key and POST the encrypted payload (web-push libraries do the RFC 8291 encryption) - payload is optional and small; the pattern is trigger-then-fetch, not attach-the-article.
- Handle it in the worker: self.addEventListener('push', e => { const data = e.data ? e.data.json() : {}; e.waitUntil(self.registration.showNotification(data.title, {body: data.body, data: {url: data.url}})); }); - e.data is binary (json()/text()/arrayBuffer()); no waitUntil, no guaranteed chance to show anything.
- Survive renewal: listen for pushsubscriptionchange in the SW - re-subscribe and POST the fresh subscription to your server - and unsubscribe() on account logout. Without the change handler, endpoint rotations silently kill your channel.
Frequently asked questions
What exactly is VAPID and why do I need a server for it?
VAPID (Voluntary Application Server Identification) is how the push ecosystem knows your server is yours: you generate a P-256 keypair, the public half goes into the browser subscribe() call, and every message your server sends is signed with the private half. The push service (Chrome uses Google's FCM endpoint, Firefox its own) rejects unsigned pushes - so push is impossible from a purely static page: there must be a server holding the private key to sign, encrypt and POST the messages. The keys in the subscription (p256dh, auth) are a separate, per-subscription encryption pair - your server encrypts each payload to them so the push service relays but cannot read. Practical setup: generate keys once (web-push generate-vapid-keys), embed the public key, guard the private key like a password - rotating it strands every existing subscription.
Why does my push arrive but show no notification - or the subscription get revoked?
Because of userVisibleOnly, the web's anti-spyware law. Chrome requires it at subscribe time and enforces it after: a push event that never calls showNotification is treated as abuse, and repeated violations get the subscription revoked (and some browsers show their own 'site was updated in the background' notice on your behalf). If you genuinely need silent work, that is Background Sync or periodicSync territory, not push. The correct shape: every push handler ends in e.waitUntil(showNotification(...)) - even if the payload is just 'something changed, tap to see'. Corollary: never gate the notification on decoding optional data - a push with an undecodable payload still legally owes the user a notification.
Do push subscriptions last forever? Mine just stopped working.
No - and the silent expiry is the API's most expensive lesson. Subscriptions are ephemeral: browsers rotate endpoints during storage cleanups, permission resets, or browser updates, and a stored endpoint that once returned 201 starts returning 404/410 from the push service. The designed answer is the pushsubscriptionchange event in the service worker: it fires when the browser re-issues a subscription, and your handler must subscribe again and ship the new subscription to your server (e.waitUntil that promise). Most broken push systems broke here - the code subscribed once in 2023 and never listened for the change event. Also handle 410 Gone responses from the push service by deleting the stored subscription server-side; keeping pushing to it just burns quota.
Does push work on iOS, and what else limits the platform?
On iOS, push exists only for web apps installed to the Home Screen (16.4+) - Safari in a normal tab has no push, which makes your A2HS install prompt part of the iOS push funnel: no install, no channel. Everywhere else the base requirement is HTTPS plus a service worker with an active registration (push is delivered to the worker, not the page). Desktop and Android browsers support the API universally, but endpoints are vendor-run services - corporate proxies sometimes block them. Two more limits: permission gating is standard (Notification.requestPermission must be user-initiated, and Chrome degrades sites that prompt on load), and payloads are best-effort with no delivery guarantee - for anything that must arrive, the payload is a knock on the door and the client fetches the real content after waking.