1. What Are Push Notifications in Web Apps?
Push notifications in web applications allow your site to send notifications to users, even when the browser is not open, as long as the service worker is active in the background. These are commonly used for:
- Reminders and alerts
- News or content updates
- Real-time chat or app messages
Push notifications rely on two main technologies:
- Service Worker: A script that runs in the background to handle push messages and display notifications.
- Push API: A browser API to subscribe the client to notifications.
They also require a secure context (https://
) or localhost during development.
2. Overview of the Architecture
Here’s a high-level view of how everything works together:
+--------------------+ +------------------+ +--------------------+ | React App | ---> | Node.js Server | ---> | Push Service | | (Frontend UI) | | (Stores subs) | | (e.g. Chrome, FCM) | +--------------------+ +------------------+ +--------------------+ | ↑ | receives notification | +------------<------------<--------+
- The React app asks the user for notification permission.
- If accepted, it registers a Service Worker and subscribes to push notifications.
- This subscription is sent to the Node.js server and stored in memory (or a database).
- Later, the server can send a message to that subscription using the Web Push Protocol.
- The browser’s Push Service delivers the notification to the user’s device.
- The Service Worker displays the notification even if the site is closed.
3. Create the application
3.1 Create a Vite + React project
npm create vite@latest react-vite-push --template react cd react-vite-push npm install
3.2 Add sw.js
in public/
directory
Create a file: public/sw.js
self.addEventListener('push', event => { const data = event.data?.json() || {}; const title = data.title || 'Notification'; const options = { body: data.body || 'You have a new message', icon: '/vite.svg' }; event.waitUntil( self.registration.showNotification(title, options) ); });
3.3 Register the service worker in main.jsx
Edit src/main.jsx
:
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.jsx'; ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <App /> </React.StrictMode> ); if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(reg => { console.log('Service Worker registered', reg); }) .catch(err => { console.error('Service Worker registration failed:', err); }); }); }
3.4 Add push subscription logic
Create a file: src/utils/subscribe.js
export async function subscribeUser() { const registration = await navigator.serviceWorker.ready; const publicVapidKey = 'YOUR_PUBLIC_VAPID_KEY'; const convertedVapidKey = urlBase64ToUint8Array(publicVapidKey); const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: convertedVapidKey }); await fetch('http://localhost:4000/subscribe', { method: 'POST', body: JSON.stringify(subscription), headers: { 'Content-Type': 'application/json' } }); console.log('Push subscription sent to server', 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(char => char.charCodeAt(0))); }
3.5 Update App.jsx
to request permission and subscribe
import { subscribeUser } from './utils/subscribe'; function App() { const handleClick = () => { if ('Notification' in window && 'serviceWorker' in navigator) { Notification.requestPermission().then(permission => { if (permission === 'granted') { subscribeUser(); } else { alert('Permission denied'); } }); } else { alert('Push not supported'); } }; return ( <div style={{ textAlign: 'center', marginTop: '2rem' }}> <h1>Vite + React Push Notification Demo</h1> <button onClick={handleClick}>Enable Notifications</button> </div> ); } export default App;
Node.js Backend to Send Push
3.6 Setup basic server
mkdir push-server cd push-server npm init -y npm install express web-push body-parser cors
3.7 Generate VAPID keys
npx web-push generate-vapid-keys
Copy both keys. You will use the public key in your frontend.
3.8 Create server.js
const express = require('express'); const webpush = require('web-push'); const bodyParser = require('body-parser'); const cors = require('cors'); const app = express(); app.use(cors()); app.use(bodyParser.json()); let subscriptions = []; const vapidKeys = { publicKey: 'YOUR_PUBLIC_VAPID_KEY', privateKey: 'YOUR_PRIVATE_VAPID_KEY' }; webpush.setVapidDetails( 'mailto:test@example.com', vapidKeys.publicKey, vapidKeys.privateKey ); app.post('/subscribe', (req, res) => { const sub = req.body; subscriptions.push(sub); res.status(201).json({ message: 'Subscription saved' }); }); app.post('/send', (req, res) => { const payload = JSON.stringify({ title: 'Hello from server!', body: 'This is a test push message.' }); Promise.all(subscriptions.map(sub => webpush.sendNotification(sub, payload))) .then(() => res.status(200).json({ message: 'Push sent' })) .catch(err => { console.error('Push error:', err); res.sendStatus(500); }); }); app.listen(4000, () => { console.log('Server running on http://localhost:4000'); });
4. Testing Your App
4.1 Start the backend server:
node server.js
4.2 Start the frontend Vite app:
npm run dev
Open your app in the browser. Click Enable Notifications.
In another terminal, trigger the push from backend:
curl -X POST http://localhost:4000/send
You should receive a browser notification, even if the React app tab is inactive.