Building Live Notifications in Under 10 Minutes
A step-by-step tutorial to add instant push notifications to any web app. Works with React, Next.js, Vue, or vanilla JavaScript. No backend WebSocket server required.
Your users shouldn't have to refresh the page to see new notifications. When someone mentions them, sends a message, or when their order ships — they should know immediately.
In this tutorial, I'll show you how to add real-time notifications to any web app in under 10 minutes. No WebSocket server to manage, no complex infrastructure. Just a few lines of code.
What We're Building
By the end of this tutorial, you'll have:
- A notification bell that updates in real-time
- Unread count badge that increments instantly
- Toast notifications that appear when events happen
- Server-side code to trigger notifications from anywhere
Prerequisites
- Any web app (React, Next.js, Vue, vanilla JS)
- A PushFlo account (free at console.pushflo.dev)
- 10 minutes
Step 1: Create Your PushFlo Account (1 minute)
- Go to console.pushflo.dev
- Sign up with email or GitHub
- Create a new app
- Copy your Publish Key and Secret Key
The free tier gives you 500,000 messages/month — more than enough to build and test.
Step 2: Install the SDK (30 seconds)
npm install @pushflodev/sdk
Or with yarn:
yarn add @pushflodev/sdk
Step 3: Create the Notification Component (3 minutes)
React / Next.js Version
// components/NotificationBell.tsx
'use client'; // For Next.js App Router
import { useState, useEffect } from 'react';
import { PushFloClient } from '@pushflodev/sdk';
interface Notification {
id: string;
title: string;
message: string;
type: 'info' | 'success' | 'warning';
read: boolean;
timestamp: string;
}
export function NotificationBell({ userId }: { userId: string }) {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [isOpen, setIsOpen] = useState(false);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const client = new PushFloClient({
publishKey: process.env.NEXT_PUBLIC_PUSHFLO_PUBLISH_KEY!,
});
client.on('connected', () => setIsConnected(true));
client.on('disconnected', () => setIsConnected(false));
// Subscribe to user-specific notification channel
client.subscribe(`notifications-${userId}`, {
onMessage: (data: Notification) => {
setNotifications(prev => [data, ...prev]);
showToast(data);
},
});
client.connect();
return () => client.disconnect();
}, [userId]);
const unreadCount = notifications.filter(n => !n.read).length;
const markAsRead = (id: string) => {
setNotifications(prev =>
prev.map(n => n.id === id ? { ...n, read: true } : n)
);
};
const markAllAsRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
};
return (
<div className="relative">
{/* Bell Icon */}
<button
onClick={() => setIsOpen(!isOpen)}
className="relative p-2 rounded-full hover:bg-gray-100"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
{/* Unread Badge */}
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center">
{unreadCount > 9 ? '9+' : unreadCount}
</span>
)}
{/* Connection Indicator */}
<span className={`absolute bottom-0 right-0 h-2 w-2 rounded-full ${
isConnected ? 'bg-green-500' : 'bg-gray-300'
}`} />
</button>
{/* Dropdown */}
{isOpen && (
<div className="absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-lg border z-50">
<div className="p-3 border-b flex justify-between items-center">
<h3 className="font-semibold">Notifications</h3>
{unreadCount > 0 && (
<button
onClick={markAllAsRead}
className="text-sm text-blue-600 hover:underline"
>
Mark all as read
</button>
)}
</div>
<div className="max-h-96 overflow-y-auto">
{notifications.length === 0 ? (
<p className="p-4 text-center text-gray-500">No notifications yet</p>
) : (
notifications.map(notification => (
<div
key={notification.id}
onClick={() => markAsRead(notification.id)}
className={`p-3 border-b cursor-pointer hover:bg-gray-50 ${
!notification.read ? 'bg-blue-50' : ''
}`}
>
<div className="flex items-start gap-3">
<span className="text-xl">
{notification.type === 'success' ? '✅' :
notification.type === 'warning' ? '⚠️' : 'ℹ️'}
</span>
<div className="flex-1">
<p className="font-medium text-sm">{notification.title}</p>
<p className="text-gray-600 text-sm">{notification.message}</p>
<p className="text-gray-400 text-xs mt-1">
{formatTime(notification.timestamp)}
</p>
</div>
{!notification.read && (
<span className="h-2 w-2 bg-blue-500 rounded-full" />
)}
</div>
</div>
))
)}
</div>
</div>
)}
</div>
);
}
// Helper function for relative time
function formatTime(timestamp: string): string {
const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
if (seconds < 60) return 'Just now';
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`;
return `${Math.floor(seconds / 86400)} days ago`;
}
// Toast notification helper
function showToast(notification: Notification) {
// Use your preferred toast library (react-hot-toast, sonner, etc.)
console.log('New notification:', notification);
}
Step 4: Add the Component to Your App (30 seconds)
// app/layout.tsx or pages/_app.tsx
import { NotificationBell } from '@/components/NotificationBell';
export default function Layout({ children }) {
const userId = getCurrentUserId(); // From your auth system
return (
<html>
<body>
<header className="flex justify-between p-4">
<Logo />
<nav className="flex items-center gap-4">
<NotificationBell userId={userId} />
<UserMenu />
</nav>
</header>
{children}
</body>
</html>
);
}
Step 5: Send Notifications from Your Backend (2 minutes)
Now the fun part — triggering notifications. Here are examples for common scenarios:
Next.js API Route
// app/api/notifications/send/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { userId, title, message, type } = await request.json();
await fetch('https://api.pushflo.dev/api/v1/publish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.PUSHFLO_SECRET_KEY}`,
},
body: JSON.stringify({
channel: `notifications-${userId}`,
data: {
id: crypto.randomUUID(),
title,
message,
type: type || 'info',
read: false,
timestamp: new Date().toISOString(),
},
}),
});
return NextResponse.json({ success: true });
}
Express.js
// routes/notifications.js
const express = require('express');
const router = express.Router();
router.post('/send', async (req, res) => {
const { userId, title, message, type } = req.body;
await fetch('https://api.pushflo.dev/api/v1/publish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.PUSHFLO_SECRET_KEY}`,
},
body: JSON.stringify({
channel: `notifications-${userId}`,
data: { id: Date.now(), title, message, type, read: false, timestamp: new Date() },
}),
});
res.json({ success: true });
});
Python (Flask/FastAPI)
# notifications.py
import requests
import os
from uuid import uuid4
from datetime import datetime
def send_notification(user_id: str, title: str, message: str, type: str = "info"):
requests.post(
"https://api.pushflo.dev/api/v1/publish",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ['PUSHFLO_SECRET_KEY']}",
},
json={
"channel": f"notifications-{user_id}",
"data": {
"id": str(uuid4()),
"title": title,
"message": message,
"type": type,
"read": False,
"timestamp": datetime.utcnow().isoformat(),
},
},
)
Step 6: Trigger Notifications from Business Events (2 minutes)
Now wire up your actual business logic:
When an order is placed:
async function createOrder(order) {
const savedOrder = await db.orders.create(order);
await sendNotification(order.userId, {
title: 'Order Confirmed',
message: `Order #${savedOrder.id} has been placed`,
type: 'success'
});
return savedOrder;
}
When someone is mentioned:
async function createComment(comment) {
const saved = await db.comments.create(comment);
// Find @mentions in the comment
const mentions = comment.text.match(/@(\w+)/g) || [];
for (const mention of mentions) {
const user = await db.users.findByUsername(mention.slice(1));
if (user) {
await sendNotification(user.id, {
title: `${comment.author.name} mentioned you`,
message: comment.text.slice(0, 100),
type: 'info'
});
}
}
return saved;
}
What You've Built
In under 10 minutes, you've added:
- Real-time notification delivery (<50ms latency)
- Unread count badge
- Notification dropdown UI
- Toast notifications for new events
- Server-side notification triggering
- Connection status indicator
- Automatic reconnection handling
All without:
- Managing WebSocket servers
- Setting up Redis pub/sub
- Complex infrastructure
- Paying for enterprise services
Next Steps
- Persist notifications — Save to your database so they survive page refreshes
- Add notification preferences — Let users choose which events they want to see
- Group similar notifications — "5 new comments on your post"
- Add notification sounds —
new Audio('/notification.mp3').play() - Implement mark-as-read sync — Update your backend when notifications are read
Ready to build more real-time features? Explore WebSockets for Vercel, learn about serverless WebSockets, or dive into the docs.
Go beyond notifications
Explore channels, presence, and live data — all on PushFlo’s free tier.
Related Articles
Do You Really Need Millions of Messages? The Real-time Pricing Trap
Why most real-time services sell you quotas you'll never use. A honest look at what indie developers actually need from WebSocket infrastructure.
How to Add Real-time Features to Next.js Without Managing Servers
Learn how to add WebSocket-powered real-time features like live notifications, chat, and dashboards to your Next.js app deployed on Vercel — without running your own WebSocket server.
WebSocket Alternatives for Vercel and Cloudflare Workers
Vercel and Cloudflare Workers don't support WebSockets natively. Here are your options for adding real-time features to serverless apps, from polling to managed services.
