Building Your Own Web Push Notification System with Node.js: ‘Breaking Up’ with Firebase for a Lighter App

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Web Push isn’t just for mobile apps

Most developers assume that if they want to send push notifications to a browser, they must use Firebase Cloud Messaging (FCM). In reality, there is a “purer” solution that allows you to fully control your infrastructure without depending on a third party: the W3C standard Web Push protocol.

I once handled a management dashboard for a logistics company with a volume of about 1,000 orders per day. At that time, integrating the Firebase SDK increased the frontend bundle size by nearly 200KB just to support a messaging feature. After switching to the web-push library on Node.js, my team completely eliminated this burden. Page load speeds increased significantly, and the deployment process became much leaner because we didn’t have to deal with complex project setups on the Google Console.

Comparing implementation methods

When integrating push notifications, you usually consider two main directions:

  • Firebase Cloud Messaging (FCM): Google acts as the intermediary. They handle the infrastructure, but in return, you must embed a heavy SDK and comply with their strict policies.
  • Web Push Protocol (VAPID): This is a direct approach. You use a VAPID (Voluntary Application Server Identification) key pair to identify your server with push services built directly into browsers like Chrome or Firefox.

Why web-push is the optimal choice for Node.js backends?

Every solution has its strengths. However, if you prioritize lean architecture and control, web-push offers very practical benefits:

Key Advantages:

  • Extremely light bundle size: You don’t need to embed a massive SDK. The frontend only needs a few lines of vanilla Service Worker code to listen for events.
  • Data Security: User subscription information stays within your own database. No one besides you and the browser knows about them.
  • Cost-Effective: It’s completely free. You will never have to worry about exceeding quotas or sudden pricing policy changes from a Cloud provider.
  • Freedom to Customize: You have full control over how to encrypt the payload and the Time-To-Live (TTL) of the notification.

Points to Consider:

  • You need to design your own database tables to store user endpoints and keys.
  • You need to write additional logic to handle retry mechanisms when message delivery fails.

How Web Push Works

For a smooth implementation, you need to understand the data flow. It doesn’t travel directly from the Server to the Browser; it goes through a relay station:

  1. Subscription: The browser requests permission from the user. If granted, it returns a subscription object containing the Push Service URL (managed by Google or Mozilla).
  2. Storage: The frontend sends this object to the Node.js Server to be stored in a database (like MongoDB or PostgreSQL).
  3. Push: When a new event occurs, the Server uses the web-push library to sign the packet with VAPID keys and sends it to the Push Service. The Push Service then pushes the notification to the user’s device, even if the website is closed.

Detailed Implementation Guide

Let’s get into the code. You only need a computer with Node.js installed.

Step 1: Project Initialization and VAPID Key Generation

First, create a directory and install the necessary packages:

mkdir web-push-tutorial
cd web-push-tutorial
npm init -y
npm install web-push express body-parser cors

The VAPID key pair acts as the server’s digital signature. Generate them quickly with this command:

./node_modules/.bin/web-push generate-vapid-keys

Copy the Public Key and Private Key. We will use them in the next steps.

Step 2: Building the Node.js Server

Create a server.js file. This is the brain that controls the notification sending process.

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

app.use(require('cors')());
app.use(require('body-parser').json());

const publicVapidKey = 'YOUR_PUBLIC_KEY';
const privateVapidKey = 'YOUR_PRIVATE_KEY';

webpush.setVapidDetails(
  'mailto:[email protected]',
  publicVapidKey,
  privateVapidKey
);

app.post('/subscribe', (req, res) => {
  const subscription = req.body;
  res.status(201).json({});

  const payload = JSON.stringify({
    title: 'System Notification',
    body: 'Your order #1234 has been confirmed!',
  });

  webpush.sendNotification(subscription, payload)
    .catch(err => console.error('Push failed:', err));
});

app.listen(5000, () => console.log('Server ready on port 5000'));

Step 3: Setting up the Client and Service Worker

On the frontend, the client.js file will handle the browser registration.

const publicVapidKey = 'YOUR_PUBLIC_KEY';

async function subscribeUser() {
  // Register Service Worker
  const register = await navigator.serviceWorker.register('/sw.js', { scope: '/' });

  // Create subscription
  const subscription = await register.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
  });

  // Send info to server
  await fetch('http://localhost:5000/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription),
    headers: { 'content-type': 'application/json' }
  });
}

Finally, sw.js. This file runs in the background to display the notification popup:

self.addEventListener('push', e => {
  const data = e.data.json();
  self.registration.showNotification(data.title, {
    body: data.body,
    icon: '/icon.png'
  });
});

Practical Tips to Avoid Errors

When bringing this system to a production environment, keep these 3 important points in mind:

  • HTTPS is Mandatory: Browsers will reject Service Workers if your website runs on HTTP (except for localhost).
  • Database Cleanup: When a user blocks notifications, the Push Service will return a 404 or 410 error. You should immediately delete that subscription from your database to avoid wasting resources.
  • Payload Limits: The maximum packet size is 4KB. Do not try to send too much data or base64 images here.

Building your own Web Push system is not as difficult as it seems. it provides absolute flexibility and makes your application more professional without depending on the Google ecosystem. Good luck with your implementation!

Share: