HexaCluster LogoHexaCluster Logo
  • Services
  • Products
  • HexaRocket
  • Blog
  • Resources
  • Company
  • Contact Us
Schedule a Demo
Stay Updated

Subscribe to Newsletters

Be the first to know! Stay updated with the latest insights, database migration benchmarks, and technical updates from HexaCluster.

HexaCluster LogoHexaCluster Logo

Enterprise-grade Database migration, modernization, and tooling for teams moving off legacy databases.

  • One Dundas Street West, Suite 2500, Toronto, Ontario, M5G 1Z3, Canada
  • HexaCluster DMCC, Plot No: JLT-PH2-RET-R6 Jumeirah Lakes Towers, Dubai, UAE
connect@hexacluster.ai+1 (902) 221-5976

Security & Compliance

SOC 2 Type 1 reportSOC 2 Type 2 report, monitored by Comp AIGDPR compliantISO 27001AICPA SOC for Service Organizations

Products

  • DMAT
  • HexaRocket
  • HexaBridge
  • HexaTranspile
  • MyBatis2Pg
  • HexaReplicate
  • Download Products

HexaRocket

  • Supported Database Migrations
  • Migrate to Yugabyte
  • About HexaRocket
  • Migrate to Oracle
  • Migrate to PostgreSQL
  • Migrate to MariaDB

Services

  • Database Migration to PostgreSQL
  • Application Migration and Modernization
  • AI/ML and MLOps
  • Architectural Health Audit
  • Managed DBA Services
  • Performance Tuning
  • PostgreSQL Development
  • Training for DBAs & Developers
  • 24/7 Support
  • Supported Tools and Extensions

Company

  • Blog
  • Case Studies
  • Webinars
  • Announcements
  • About Us
  • Referral Program
  • Events
  • Contact Us

© HexaCluster 2026. All rights reserved. Privacy PolicyThis site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

HEXACLUSTERHEXACLUSTERHEXACLUSTER

Implementing Browser Push Notifications in React.js and Next.js with Web Push

Uday Kiran Bandarugalla
Feb 17, 2026
Push NotificationsWebFrontend#reactjs#nextjs#push-notifications+3 more

Push notifications are one of the most effective ways to keep users engaged by delivering timely updates directly to their devices. On the web, browser push notifications allow applications to send messages even when the user is not actively visiting the website.

In this article, we’ll explore how browser push notifications work and how to implement them in React.js and Next.js applications.


image


Before that we will learn about the basics. i.e. Notification API

  1. Notification API

    The Notification API is a browser-provided Web API that allows web application to display system-level notifications to users. These notifications are handled by the operating system, not the web page itself, which means they can appear outside the browser UI.

    The Notification API is only responsible for displaying notifications. It does not send messages from a server or work in the background by itself. For background delivery, it must be combined with Service Workers and the Push API, which we’ll cover later in this article.

  2. How to Request Permission

    Before a website can show any notification, it must explicitly ask the user for permission. Browsers enforce this to protect users from spam and abuse.

    const permission = await Notification.requestPermission();
    
    console.log(permission); // "denied" or "granted" or "default"
    
    

    The requestPermission() method returns one of three values:

    • "granted" → The user has allowed notifications
    • "denied" → The user has blocked notifications
    • "default" → The user dismissed the permission prompt without making a choice
  3. Push new Notification

    Once permission is granted, you can create and display a notification using the Notification constructor.

    // Push a Notification
    const button = document.getElementById("button");
    button.addEventListener("click", async () => {
      const permission = await Notification.requestPermission();
      if (permission === "denied") {
        alert("Permission denied");
        return;
      }
      new Notification("Hello world!");
    });
    

    In this example:

    • A notification permission request is triggered when the user clicks a button
    • If permission is granted, a notification with a title is displayed immediately
    • The notification is shown only while the page is open

    This approach is useful for simple, in-page alerts, but it does not support background notifications.

  4. Other Options to push notifications

    The Notification API supports several options to enhance the user experience.

  • You can also add notification body

    You can include additional text using the body option to provide more context.

    new Notification("Hello From Uday!",{
        body: "This is a new notification",
    });
    
  • show notification with icon

    Icons help users quickly recognize the source of the notification.

    new Notification("Hello From Uday!", {
      body: "This is a new notification",
      icon: "https://udaykiran.dev/assets/uday.jpeg",
    });
    
  • Prevent Duplicate Notifications

    You can prevent multiple notifications from stacking by using the tag property.

    If a notification with the same tag already exists, the browser replaces it instead of creating a new one. This is useful for status updates or progress notifications.

    new Notification("Hello From Uday!", {
        body: "This is a new notification",
        icon: "https://udaykiran.dev/assets/uday.jpeg",
        tag: "unique-tag",
      });
    
  • Listen to events on Notification

    Notifications support events such as click and close, allowing you to respond to user interactions.

    const notification = new Notification("Hello From Uday!", {
      body: "This is a new notification",
      icon: "https://udaykiran.dev/assets/uday.jpeg",
      data: {
        name: "Uday",
      },
    });
    
    notification.addEventListener("click", () => {
      alert("Notification clicked");
    });
    // access the notification data
    alert(notification.data?.name);
    

    Here:

    • The data property allows you to attach custom metadata
    • This data can be accessed later when handling notification events
  • close notification automatically

    By default, notifications are closed by the browser or the user. However, you can programmatically close them after a certain time.

    const notification = new Notification("Hello From Uday!", {
      body: "This is a new notification",
      icon: "https://udaykiran.dev/assets/uday.jpeg",
    });
    setTimeout(() => {
      notification.close();
    }, 500);
    

Notifications created using the Notification API only work while the page is open. To send notifications when the website is closed, we need Service Workers and the Push API, which we’ll explore next.

Push Notifications

Push notifications in web applications involve a combination of browser APIs, Service Workers, and a server that can send push messages to subscribed users.

Once a user subscribes to push notifications, the browser creates a secure communication channel between the application server and the user’s device. Even if the website is not currently open, the server can still deliver messages through the browser’s push service.

If the browser is offline, the push message is queued by the push service and delivered as soon as the browser comes back online. Users can choose whether or not to receive push notifications from a website, and they can revoke this permission at any time.

Service worker

Service worker is a script that runs in the background of a web application, separate from the web page. It enables features like background sync, push notifications, and caching.

For push notifications, the service worker acts as the bridge between the push message and the Notification API.

1. Create Service Worker (Handling Push Events)

The service worker listens for incoming push messages using the push event. When a push message is received, the service worker extracts the payload and displays a notification using the Notification API.

// public/sw.js
self.addEventListener("push", (event) => {
  const data = event.data.json();
  const title = data.title;
  const body = data.body;
  const icon = data.icon;
  const url = data.data.url;

  const notificationOptions = {
    body: body,
    tag: "unique-tag", // Use a unique tag to prevent duplicate notifications
    icon: icon,
    data: {
      url: url, // we can store some data for future use (url is used to navigate / open this url in the browser upon clicking on the notification)
    },
  };

  self.registration.showNotification(title, notificationOptions);
});

What’s happening here:

  • The push event is triggered when the browser receives a push message from the server
  • The payload sent from the server is accessed using event.data.json()
  • showNotification() displays the notification using the Notification API
  • The data object is attached to the notification for later use (for example, redirecting the user when the notification is clicked)

2. Registering Service Worker in React and Next.js

Before the browser can receive push messages, the service worker must be registered from the client-side code.

// app/page.js -> Next.js
// src/index.js -> React

useEffect(() => {
  if ("serviceWorker" in navigator) {
    navigator.serviceWorker
      .register("/sw.js")
      .then((registration) => {
        console.log("Registration successful");
      })
      .catch((error) => {
        console.log("Service worker registration failed");
      });
  }
}, []);

3. Sending Push Notification from Server

Push notifications cannot be sent directly from the browser. They must originate from a server that uses the Web Push protocol.

pnpm i web-push

The web-push library handles:

  • Encryption of payloads
  • Authentication using VAPID keys
  • Communication with browser push services

4. Create / VAPID key pair

VAPID keys are used to identify your application server and authenticate push requests.

npx web-push generate-vapid-keys

This command generates:

  • A public key (shared with the browser)
  • A private key (kept secret on the server)

5. Server Implementation

Below is a simple Express server that:

  • Stores push subscriptions
  • Sends push notifications to all subscribed users
// server.js

import express from "express";
import webpush from "web-push";
import dotenv from "dotenv";
dotenv.config();

const app = express();

const vapidKeys = {
	publicKey: process.env.VAPID_PUBLIC_KEY,
	privateKey: process.env.VAPID_PRIVATE_KEY,
};

webpush.setVapidDetails(
	"mailto:connect@hexacluster.ai", // for identifying your server
  vapidKeys.publicKey,
  vapidKeys.privateKey
);

const subscriptions = [];

VAPID Setup Explanation:

  • mailto:connect@hexacluster.ai is the VAPID subject, used to identify your server
  • The public and private keys are used to sign push requests
  • Browsers verify this signature before delivering messages

Storing Subscriptions

app.post("/subscribe",(req, res) => {
const subscription = req.body;
  // storing in array 
  subscriptions.push(subscription);
  res.status(201).json({status:"success" });
});

This endpoint:

  • Receives a push subscription from the client
  • Stores it (in memory for demo purposes)
  • In production, subscriptions should be stored in a database

🔹 HexaRocket Insight
By the way, if you’re into databases and database migrations, check out our robust tool, HexaRocket, an advanced, unified database migration and replication engine by HexaCluster that streamlines data movement across databases.

Sending Push Notifications

app.post("/send-notification", async (req, res) => {
  const notificationPayload = {
    title: "New Notification",
    body: "This is a new notification",
    icon: "https://udaykiran.dev/assets/uday.jpeg",
    data: {
      url: "https://example.com",
    },
  };

  try {
    const sendPromises = subscriptions.map((subscription, index) =>
      webpush
        .sendNotification(
          subscription,
          JSON.stringify(notificationPayload)
        )
        .catch((error) => {
          // 🔴 Subscription is no longer valid
          if (error.statusCode === 410 || error.statusCode === 404) {
            console.log("Removing expired subscription");

            // Remove invalid subscription from DB / memory
            subscriptions.splice(index, 1);
          } else {
            console.error("Push error:", error);
          }
        })
    );

    await Promise.all(sendPromises);

    res.status(200).json({
      message: "Notification sent successfully.",
    });
  } catch (err) {
    console.error("Error sending notifications", err);
    res.sendStatus(500);
  }
});

What happens here:

  • The server iterates over all stored subscriptions
  • A push message is sent to each subscription
  • The payload is encrypted and delivered to the browser
  • The service worker receives the message and shows the notification

6. Subscribing to Push Notifications (Client Side)

To receive push notifications, a user must explicitly subscribe to them. This process happens entirely in the browser

Step 1: Ask for Notification Permission

Browsers require user permission before showing notifications.

const permission = await Notification.requestPermission();
if (permission !== "granted")return;

Step 2: Create a Push Subscription

After permission is granted and the service worker is ready, create a subscription using PushManager.

const registration = await navigator.serviceWorker.ready;

const subscription = await registration.pushManager.subscribe({
	userVisibleOnly : true,
	applicationServerKey : process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
});

This subscription contains:

  • Push endpoint (where notifications are sent)
  • Encryption keys (used by the server)

Step 3: Send Subscription to Server

The server must store this subscription to send notifications later.

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

Full Example (React / Next.js)

"use client" 

async function subscribeToPush() {
  if (!("serviceWorker" in navigator)) return;

  const permission = await Notification.requestPermission();
  if (permission !== "granted") return;
  
  const registration = await navigator.serviceWorker.ready;
	// assuming service worker registration is already done 
	// if not un comment below line 
	// await navigator.serviceWorker.register("/sw.js") 

  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
  });
  // send subscription to backend to store 
  await fetch("http://localhost:4000/subscribe", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(subscription),
  });
}

Now that we've covered subscribing to push notifications, let's look at how to handle user interactions with notifications and manage subscriptions effectively.

Handling Notification Clicks

When users click on a notification, you'll typically want to:

  • Open a specific page in your application
  • Focus an existing tab if it's already open
  • Pass contextual data to determine the destination

Here's a complete implementation:

// public/sw.js

self.addEventListener("notificationclick", (event) => {
  const urlToOpen = event.notification.data?.url;
  event.notification.close(); // Close the notification
ams
  self.event.waitUntil(
    clients.matchAll({
      type: "window",
      includeUncontrolled: true
    }).then((clientList) => {
      // Check if a window with this URL is already open
      for (const client of clientList) {
        if (client.url === urlToOpen && "focus" in client) {
          return client.focus(); // Focus existing tab
        }
      }
      // If no matching window found, open a new one
      return self.clients.openWindow(urlToOpen);
    })
  );
});

Why this approach is better:

  • Prevents duplicate tabs - If the target page is already open, it focuses that tab instead of opening a new one
  • Better UX - Users don't end up with multiple tabs of the same page
  • Resource efficient - Reuses existing browser windows

Advanced Topics & Production Considerations

1. Unsubscribing from Push Notifications

Users should always have the option to opt-out of notifications:

async function unsubscribeFromPush() {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.getSubscription();

  if (subscription) {
    await subscription.unsubscribe();

    // Notify your server to remove the subscription
    await fetch("<http://localhost:4000/unsubscribe>", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ endpoint: subscription.endpoint }),
    });

    console.log("Successfully unsubscribed");
  }
}

Server-side cleanup:

app.post("/unsubscribe", (req, res) => {
  const { endpoint } = req.body;

  // Remove from database
  const index = subscriptions.findIndex(sub => sub.endpoint === endpoint);
  if (index !== -1) {
    subscriptions.splice(index, 1);
  }

  res.status(200).json({ status: "unsubscribed" });
});

2. Storing Subscriptions in a Database (Production Setup)

Don't store subscriptions in memory! Use a database for production:

// Example with MongoDB/Mongoose

const SubscriptionSchema = new mongoose.Schema({
  endpoint: { type: String, required: true, unique: true },
  keys: {
    p256dh: String,
    auth: String
  },
  userId: String, // Associate with your user
  createdAt: { type: Date, default: Date.now }
});

// Save subscription
app.post("/subscribe", async (req, res) => {
  const { endpoint, keys } = req.body;

  try {
    await Subscription.findOneAndUpdate(
      { endpoint },
      { endpoint, keys, userId: req.user.id },
      { upsert: true }
    );
    res.status(201).json({ status: "success" });
  } catch (error) {
    res.status(500).json({ error: "Failed to save subscription" });
  }
});

🔹 HexaRocket Recommendation
Here, we’ve used a MongoDB schema as a simple example, you’re free to choose any database that fits your use case. If you have a legacy database and want to migrate to open-source databases, check out our robust tool HexaRocket an advanced migration and replication engine that enables seamless data transfer across different database technologies.

3. Handling Expired Subscriptions (410 Gone)

Browser subscriptions can expire. Your server must handle this gracefully:

app.post("/send-notification", async (req, res) => {
  const subscriptions = await Subscription.find({});

  const results = await Promise.allSettled(
    subscriptions.map(async (sub) => {
      try {
        await webpush.sendNotification(
          { endpoint: sub.endpoint, keys: sub.keys },
          JSON.stringify(notificationPayload)
        );
      } catch (error) {
        // Subscription is invalid or expired
        if (error.statusCode === 410 || error.statusCode === 404) {
          // Remove from database
          await Subscription.deleteOne({ endpoint: sub.endpoint });
          console.log(`Removed expired subscription: ${sub.endpoint}`);
        }
        throw error;
      }
    })
  );

  res.json({ sent: results.filter(r => r.status === 'fulfilled').length });
});

4. Checking Subscription Status

Allow users to see if they're currently subscribed:

async function getSubscriptionStatus() {
  if (!("serviceWorker" in navigator)) return false;

  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.getSubscription();

  return subscription !== null;
}

// Usage in React/Next.js
const [isSubscribed, setIsSubscribed] = useState(false);

useEffect(() => {
  getSubscriptionStatus().then(setIsSubscribed);
}, []);

5. Notification Actions (Interactive Buttons)

Add action buttons to notifications for richer interactions:

// public/sw.js

self.addEventListener("push", (event) => {
  const data = event.data.json();

  const options = {
    body: data.body,
    icon: data.icon,
    actions: [
      { action: "view", title: "View" },
      { action: "dismiss", title: "Dismiss" }
    ],
    data: data.data
  };

  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

// Handle action clicks
self.addEventListener("notificationclick", (event) => {
  event.notification.close();

  if (event.action === "view") {
    event.waitUntil(clients.openWindow(event.notification.data.url));
  } else if (event.action === "dismiss") {
    // Just close, no action needed
  }
});

6. Silent Push (Background Sync)

Sometimes you want to receive a push without showing a notification:

// public/sw.js

self.addEventListener("push", (event) => {
  const data = event.data.json();

  if (data.silent) {
    // Perform background task without showing notification
    event.waitUntil(
      fetch("/api/sync", {
        method: "POST",
        body: JSON.stringify({ type: "background-sync" })
      })
    );
  } else {
    // Show notification
    self.registration.showNotification(data.title, {
      body: data.body
    });
  }
});

⚠️ Warning: Browsers may require you to show at least one notification per push to prevent abuse.

8. Notification Permission Best Practices

❌ Don't do this:

// Asking immediately on page load
useEffect(() => {
  Notification.requestPermission();
}, []);

✅ Do this instead:

// Ask in context, with explanation
<button onClick={handleSubscribe}>
  Get notified when new posts arrive
</button>

async function handleSubscribe() {
  // Show a modal explaining benefits first
  const userWantsNotifications = await showExplanationModal();

  if (userWantsNotifications) {
    const permission = await Notification.requestPermission();
    if (permission === "granted") {
      await subscribeToPush();
    }
  }
}

9. Service Worker Updates

Service workers are cached aggressively. Force updates:

// Update service worker when new version is available
useEffect(() => {
  if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register("/sw.js").then((registration) => {
      // Check for updates every hour
      setInterval(() => {
        registration.update();
      }, 3600000);

      // Listen for new service worker
      registration.addEventListener("updatefound", () => {
        const newWorker = registration.installing;
        newWorker.addEventListener("statechange", () => {
          if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
            // New service worker available
            if (confirm("New version available! Reload to update?")) {
              window.location.reload();
            }
          }
        });
      });
    });
  }
}, []);

Conclusion

Push notifications are a powerful way to re-engage users and deliver timely updates. In this guide, we covered:

✅ The Notification API and basic browser notifications

✅ Service Workers and the Push API

✅ VAPID authentication and server setup

✅ Complete client and server implementation

✅ Production considerations and best practices

Interested in learning about micro frontends and Module Federation? Read this blog post: https://hexacluster.ai/blog/getting-started-with-micro-frontends-and-module-federation

Key Takeaways:

  • Always ask for permission contextually, not immediately on page load
  • Store subscriptions in a database, not in memory
  • Handle expired subscriptions gracefully (410 Gone)
  • Provide easy opt-out options
  • Keep notifications valuable and non-spammy

Next Steps:

  • Implement analytics to track notification engagement
  • Add notification scheduling for optimal delivery times

We’ve implemented push notifications on our site, feel free to subscribe to stay updated when we publish new blogs and features 😌

Resources

  • MDN Web Push API
  • web-push npm package
  • MDN Notification API
  • web-push package documentation

HexaCluster provides end-to-end migration and modernization services, including application migration and modernization, database migration, and PostgreSQL consulting such as performance tuning, health audits, managed DBA services, and 24/7/265 support.

To start a conversation or explore how we can support your migration journey, please contact us at connect@hexacluster.ai

Authors

Uday Kiran Bandarugalla

Uday Kiran Bandarugalla

Software Development Engineer

Udaykiran is a Software Developer at HexaCluster with 1.5+ years of experience building fast, scalable web applications. He works across the full stack, crafting modern frontends using Next.js, React, TypeScript, and micro-frontends, and powering them with robust backend services built with Node.js, Spring Boot, and Golang. With hands-on experience in microservices architecture, he builds modular, production-ready systems end to end.

Start your migration journey 🚀

start your migration journey with our expert team

Products

DMAT

Database & Application Migration Assessment Tool

HexaRocket

End-to-End Database Migration & Modernization Tool

HexaTranspile

Database Code Object Conversion to PostgreSQL

MyBatis2Pg

MyBatis Mapper Conversion to PostgreSQL

HexaReplicate

Enterprise Data Replication & Live CDC

HexaBridge

Oracle Compatibility Layer for PostgreSQL

HexaRocket 🚀

Oracle to PostgreSQLSQL Server to PostgreSQLMySQL to PostgreSQLMariaDB to PostgreSQLAny to Any databases

Migration Services

Database MigrationsApplication Modernization

PostgreSQL Consulting

Architectural AuditsPerformance TuningTraining & Support