DEV Community

AYUSH SRIVASTAVA
AYUSH SRIVASTAVA

Posted on

Ship real‑time alerts without WebSocket's: Web Push for enterprise constraints 🔔

Some organizations restrict persistent connections like WebSockets, yet teams still need timely notifications even when the app isn’t open or focused.
With Web Push—the Push API, a Service Worker, and VAPID—servers can push messages reliably without keeping a socket alive, including when the page is backgrounded or closed.

Why Web Push

  • Works in the background via a Service Worker and shows native notifications using the Notifications API for consistent, system‑level UX.
  • Standards‑based, requires HTTPS, and uses VAPID keys so your server is identified securely to push services.

How it fits together

  • App registers a Service Worker and requests notification permission from the user on a secure origin.
  • App subscribes with Push Manager to get a unique subscription endpoint and keys for that browser/device.
  • Server stores subscriptions and later sends payloads signed with VAPID using a lightweight library.
  • The Service Worker receives the push event and displays a native notification immediately.

Client: register SW and subscribe

// Convert base64 VAPID public key to Uint8Array
function base64ToUint8Array(base64) {
  const padding = '='.repeat((4 - (base64.length % 4)) % 4);
  const b64 = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/');
  const raw = atob(b64);
  const output = new Uint8Array(raw.length);
  for (let i = 0; i < raw.length; ++i) output[i] = raw.charCodeAt(i);
  return output;
}

async function subscribeToPush(vapidPublicKeyBase64) {
  const registration = await navigator.serviceWorker.register('/sw.js');
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') return;

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

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

Enter fullscreen mode Exit fullscreen mode

The app subscribes via the Push API on a secure context and sends the resulting subscription to the backend for later use.

Service Worker: receive and notify


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

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

Enter fullscreen mode Exit fullscreen mode

The Service Worker handles the push event payload and displays a native notification using the Notifications API.

Server (Node/Express): VAPID and send


// npm i express web-push
import express from 'express';
import webpush from 'web-push';

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

// 1) Configure VAPID (generate once and set via env)
webpush.setVapidDetails(
  'mailto:admin@example.com',
  process.env.VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY
);

// 2) Store subscriptions (replace with MongoDB in production)
const subscriptions = new Map();

app.get('/api/push/public-key', (_req, res) => {
  res.json({ publicKey: process.env.VAPID_PUBLIC_KEY });
});

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

app.post('/api/push/send', async (req, res) => {
  const payload = JSON.stringify({
    title: 'Policy update',
    body: 'Click to review changes',
    url: '/inbox',
  });
  const results = [];
  for (const sub of subscriptions.values()) {
    try {
      await webpush.sendNotification(sub, payload);
      results.push({ ok: true });
    } catch {
      results.push({ ok: false });
    }
  }
  res.json({ sent: results.length });
});

app.listen(3000, () => console.log('Server running on 3000'));

Enter fullscreen mode Exit fullscreen mode

The web‑push library signs payloads with VAPID and delivers to each saved subscription endpoint, letting servers send messages without maintaining a persistent connection.

Practical tips

  • Only request permission at meaningful moments to avoid prompt fatigue and improve opt‑in rates.
  • Subscriptions can expire; handle send failures by pruning invalid endpoints and re‑subscribing when needed.
  • Push requires HTTPS and secure contexts; keep VAPID keys safe and reuse the same key pair across deploys per environment policy

If WebSocket's are off the table, Web Push gives reliable, secure, background delivery with a small footprint—perfect for “must‑know” alerts in constrained environments.

Top comments (0)