How to Implement Push Notifications in a Progressive Web App: Step-by-Step

How to Implement Push Notifications in a Progressive Web App: Step-by-Step

by | Jul 14, 2026 | Uncategorized | 0 comments

Push notifications are one of the most powerful tools to bring users back to your app, and the good news is that you no longer need a native iOS or Android build to send them. With a properly configured Progressive Web App (PWA), you can re-engage users directly from the browser, even when your app is closed.

In this tutorial, we will walk through the complete process of implementing push notifications in a progressive web app: from generating VAPID keys, to handling permissions in the browser, registering a service worker, and sending notifications from a Node.js server. By the end, you will have a working push notification pipeline you can drop into any PWA.

Why Push Notifications Matter for PWAs

PWAs combine the reach of the web with the engagement features of native apps. Push notifications close one of the last remaining gaps between web and native experiences. They allow you to:

  • Re-engage users who installed your PWA but have not opened it recently
  • Send transactional alerts (orders, messages, security events)
  • Deliver marketing and content updates without an app store
  • Reduce churn without the cost of maintaining a native codebase

As of 2026, push notifications are supported in Chrome, Edge, Firefox, Opera, and Safari (iOS 16.4+ on installed PWAs). Coverage is now wide enough that web push is a serious alternative to native messaging.

phone notification alert

How Push Notifications Work in a PWA

Before writing code, it helps to understand the moving parts. A web push setup involves four actors:

Component Role
Browser (Client) Requests permission and subscribes the user to push messages.
Service Worker Runs in the background and receives push events to display notifications.
Push Service Provided by the browser vendor (FCM, Mozilla autopush, Apple). Delivers the message.
Application Server Your Node.js backend that triggers and signs the push payload using VAPID.

Step 1: Generate Your VAPID Keys

VAPID (Voluntary Application Server Identification) is the authentication mechanism that proves your server is allowed to send messages to a given subscription. You need a public/private key pair.

Install the web-push library and generate keys:

npm install web-push
npx web-push generate-vapid-keys

You will get something like:

Public Key:
BNb...your_public_key...

Private Key:
Xyz...your_private_key...

Important: store the private key in an environment variable, never in your client bundle.

phone notification alert

Step 2: Register the Service Worker

In your main JavaScript file (or app entry point), register a service worker:

if ('serviceWorker' in navigator && 'PushManager' in window) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered', reg))
    .catch(err => console.error('SW registration failed', err));
}

Create sw.js in your public root:

self.addEventListener('push', event => {
  const data = event.data ? event.data.json() : {};
  const title = data.title || 'New notification';
  const options = {
    body: data.body || '',
    icon: '/icons/icon-192.png',
    badge: '/icons/badge-72.png',
    data: { url: data.url || '/' }
  };
  event.waitUntil(self.registration.showNotification(title, options));
});

self.addEventListener('notificationclick', event => {
  event.notification.close();
  event.waitUntil(clients.openWindow(event.notification.data.url));
});

Step 3: Request Permission and Subscribe the User

Push notifications require explicit user consent. Best practice is to ask only after a meaningful interaction, never on page load.

async function subscribeUser() {
  const registration = await navigator.serviceWorker.ready;

  const permission = await Notification.requestPermission();
  if (permission !== 'granted') {
    console.warn('Push permission denied');
    return;
  }

  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
  });

  await fetch('/api/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription)
  });
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
  const rawData = atob(base64);
  return Uint8Array.from([...rawData].map(c => c.charCodeAt(0)));
}

Step 4: Build the Node.js Server

Now the backend. Here is a minimal Express server that stores subscriptions and sends pushes:

const express = require('express');
const webpush = require('web-push');

const app = express();
app.use(express.json());

webpush.setVapidDetails(
  'mailto:[email protected]',
  process.env.VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY
);

const subscriptions = []; // use a real DB in production

app.post('/api/subscribe', (req, res) => {
  subscriptions.push(req.body);
  res.status(201).json({ ok: true });
});

app.post('/api/notify', async (req, res) => {
  const payload = JSON.stringify({
    title: req.body.title,
    body: req.body.body,
    url: req.body.url
  });

  const results = await Promise.allSettled(
    subscriptions.map(sub => webpush.sendNotification(sub, payload))
  );

  res.json({ sent: results.length });
});

app.listen(3000, () => console.log('Push server running on 3000'));
phone notification alert

Step 5: Test Your Push Notifications

Run your PWA over HTTPS (or localhost for development), subscribe a user, then trigger a notification:

curl -X POST http://localhost:3000/api/notify \
  -H "Content-Type: application/json" \
  -d '{"title":"Hello PWA","body":"Your first push works!","url":"/dashboard"}'

You should see the notification appear, even if the browser tab is closed (but the browser process must still be running, or the OS must support background wake).

Handling Edge Cases and Best Practices

Expired or invalid subscriptions

When you call sendNotification, the push service may return a 410 Gone or 404. Catch these errors and remove the subscription from your database to avoid sending to dead endpoints.

iOS-specific rules

On iOS, web push only works if the user has added the PWA to their Home Screen. You should detect this state and guide users with an install prompt before requesting notification permission.

Permission UX

  • Never request permission immediately on page load
  • Explain the value before the native prompt appears (a custom “pre-prompt” UI)
  • Provide a way to re-enable notifications inside your app settings

Payload size

Keep payloads under 4 KB. For richer content, send a small payload with an ID and let the service worker fetch full data from your API.

phone notification alert

Common Pitfalls to Avoid

  1. Forgetting userVisibleOnly: true: silent push is not allowed on the web
  2. Hardcoding the private VAPID key in client code
  3. Skipping HTTPS: push only works on secure origins
  4. Not handling the notificationclick event, leaving users with a dead notification
  5. Spamming users: high opt-out rates damage long-term engagement

Comparison: Web Push vs Native Push

Feature Web Push (PWA) Native Push
App store needed No Yes
Cross-platform code Single codebase iOS + Android separate
iOS support Installed PWAs only Full
Silent / background push Limited Full
Setup cost Low High

FAQ

Can a progressive web app send push notifications without a native app?

Yes. With the Web Push API, a service worker, and VAPID authentication, your PWA can deliver notifications on Chrome, Edge, Firefox, and Safari (when installed on iOS).

Do PWA push notifications work when the app is closed?

Yes, on most desktop and Android setups, the service worker can wake up to display a notification even when the PWA is not open. On iOS, the device must be online and the PWA must be installed.

Do I need Firebase for PWA push notifications?

No. Firebase Cloud Messaging is one option, but the open Web Push protocol with VAPID works directly with all major browsers and gives you more control without vendor lock-in.

What is the difference between the Notifications API and the Push API?

The Notifications API displays a message on the user’s device. The Push API delivers a message from your server to the browser, which then uses the Notifications API to show it. You typically need both.

How can I test push notifications locally?

Use localhost (treated as secure), Chrome DevTools’ Application tab to inspect service workers and trigger push events, and tools like ngrok when you need a real HTTPS URL.

Wrapping Up

Adding push notifications to your PWA is a high-leverage feature: a few hundred lines of code can dramatically improve retention without forcing you into the app store ecosystem. Start small, respect user attention, and iterate based on engagement metrics.

At Box Software, we help product teams design and ship modern PWAs with native-grade features like push, offline mode, and background sync. If you want help integrating push notifications into your product, get in touch with our team.