PushFlo
Back to Blog
7 min readBy Marek

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.

notificationstutorialreactjavascriptreal-time

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)

  1. Go to console.pushflo.dev
  2. Sign up with email or GitHub
  3. Create a new app
  4. 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 soundsnew 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.