Skip to content
Developers · 8 min read

Web push notifications with Node.js

The web-push package handles encryption and VAPID signing for you. With Express and a small database table, you can send standard push to every browser in under 50 lines.

Updated By the Pusharoo team
In short
  • npm install web-push gives you sendNotification() with encryption and VAPID built in.
  • Generate keys once with npx web-push generate-vapid-keys and keep the private key in env vars.
  • Store each PushSubscription (endpoint + keys) server-side, keyed by endpoint.
  • Delete subscriptions that return 404 or 410.

Install and configure

npm install web-push express npx web-push generate-vapid-keys # prints a public and a private key once
// push.js const webpush = require('web-push'); webpush.setVapidDetails( 'mailto:[email protected]', process.env.VAPID_PUBLIC_KEY, process.env.VAPID_PRIVATE_KEY ); module.exports = webpush;

Save subscriptions with Express

const express = require('express'); const app = express(); app.use(express.json()); app.post('/api/push/subscriptions', async (req, res) => { const { endpoint, keys } = req.body || {}; if (!endpoint || !keys?.p256dh || !keys?.auth) return res.sendStatus(400); await db.subscriptions.upsert({ endpoint, p256dh: keys.p256dh, auth: keys.auth }); res.sendStatus(201); });

The browser side that produces this JSON is in the Push API example.

Send and clean up

const webpush = require('./push'); async function sendToAll(message) { const payload = JSON.stringify(message); // { title, body, url, icon } const subs = await db.subscriptions.all(); for (const s of subs) { const subscription = { endpoint: s.endpoint, keys: { p256dh: s.p256dh, auth: s.auth } }; try { await webpush.sendNotification(subscription, payload, { TTL: 3600, urgency: 'normal' }); } catch (err) { if (err.statusCode === 404 || err.statusCode === 410) await db.subscriptions.remove(s.endpoint); else console.error('push failed', err.statusCode, err.body); } } }

For large lists, send with limited concurrency (for example 50–100 in flight) and a queue, so a slow push service doesn’t block your app.

Useful options

OptionPurpose
TTLSeconds to keep trying if the device is offline
urgencyvery-low … high
topicReplace an undelivered message with the same topic
headersExtra request headers if needed

What these mean on the wire: the Web Push protocol.

A tiny concurrency limiter

async function sendAll(subs, payload, limit = 50) { let i = 0; const worker = async () => { while (i < subs.length) { const s = subs[i++]; try { await webpush.sendNotification(s, payload, { TTL: 3600 }); } catch (e) { if (e.statusCode === 404 || e.statusCode === 410) await db.subscriptions.remove(s.endpoint); } } }; await Promise.all(Array.from({ length: limit }, worker)); }

For bigger lists, move this into a job queue (BullMQ, a cron worker, etc.) and record sent/failed counts per campaign.

Production checklist

  • Private key in env/secrets only; never in the client bundle.
  • Rate-limit the subscription endpoint and validate input.
  • Queue sends; retry 429/5xx with backoff; never retry 404/410.
  • Log sent/failed counts per campaign; add click tracking via your notification URLs.
  • Serve sw.js uncached from your root.

Rather not maintain this? Pusharoo gives you the same standards-based delivery with a dashboard, segments, schedules and automations, free (docs).

Questions

Does the web-push npm package need Firebase?

No. It speaks the standard Web Push protocol to every browser’s push service using VAPID.

Can I use web-push with TypeScript or ES modules?

Yes. Import it with import webpush from 'web-push' and use the same API; type definitions are available.

How do I send to thousands of subscribers quickly?

Use a job queue and limited concurrency, and remove expired subscriptions as you go so later sends are faster.

More in Developers: Service worker push events, done right · The Web Push protocol, explained · Laravel browser push notifications (no Firebase) · Declarative Web Push, explained · Firebase web push vs. standard web push · All guides

Free for life. No limits.

Unlimited subscribers, campaigns and automations. No badges. About five minutes to install.