Real-time Notification Center Implementation
Imagine your SaaS product processing hundreds of events per minute – new orders, comments, task statuses. Users complain they miss important notifications, and the admin panel is overloaded. On one project, 20,000 users simultaneously generated 500 notifications per second, and without proper architecture, the system crashed. After implementing our solution, server load decreased by 70%. We solved this problem by deploying a real-time notification center built on Laravel + WebSocket + React. Under the hood: an optimized database model, asynchronous delivery, and well-thought-out UX.
Problems Solved by the Notification Center
Traditional polling solutions create excessive server load: a database query every 5 seconds. With 1,000 active users, that's 200 RPS just for checking notifications. WebSocket (Laravel Echo + Pusher) provides a push model: notifications arrive instantly without extra requests. This reduces TTFB by 3–4 times and improves Core Web Vitals. For example, after implementation on a project with 5,000 DAU, complaints about notification delays dropped by 90%. Another issue is lost notifications due to broken architecture: the client may miss an event if the WebSocket connection is lost. Our solution uses Laravel queues with automatic retries and a delivery confirmation mechanism.
WebSocket Driver Selection
| Driver | Hosting | Scaling | Latency | Features |
|---|---|---|---|---|
| Pusher | SaaS | Built-in | <20ms | Quick start, fixed monthly fee |
| Soketi | Self-hosted | Horizontal via Redis | <50ms | Free, full control |
| Laravel Reverb | Self-hosted | Horizontal via Redis | <30ms | Open source, Laravel compatibility |
Pusher is best for quick start, Soketi and Reverb for full control and cost savings with many users. The choice depends on SLA requirements: SaaS provides predictable cost, self-hosted lowers infrastructure expenses. For small projects, Pusher is enough; for enterprise systems with tens of thousands of connections, Soketi is better.
Laravel Broadcasting enables real-time push notifications via WebSocket, reducing latency to under 50ms.
| Notification Type | Description | Example Data |
|---|---|---|
| System | Error, warning | 'Your licenses have expired' |
| Social | Likes, comments | 'User X left a comment' |
| Business event | Order, payment | 'New order #123' |
Database Structure and Indexes
Why indexes in the notifications table matter?
Without indexes, queries for unread notifications scan the entire table. On a table with millions of rows, this leads to timeouts. We use a covering index on (user_id, read_at, created_at DESC), which speeds up sorting and filtering by 10 times. The notifications table can reach 10 million rows, and without indexes, queries take 5–10 seconds instead of 50 ms. This structure ensures fast inserts and selects, with the index covering 95% of queries.
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(100) NOT NULL,
icon VARCHAR(50),
title VARCHAR(255),
body TEXT,
url VARCHAR(500),
data JSONB NOT NULL DEFAULT '{}',
read_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ON notifications(user_id, read_at, created_at DESC);
Optionally, add an index on type for filtering by category.
Laravel: API Endpoints and Broadcasting
class NotificationController extends Controller
{
public function index(Request $request): JsonResponse
{
$notifications = auth()->user()->notifications()
->latest()
->limit(50)
->get();
return response()->json([
'notifications' => NotificationResource::collection($notifications),
'unread_count' => $notifications->whereNull('read_at')->count(),
]);
}
public function markRead(Request $request): JsonResponse
{
$query = auth()->user()->notifications()->whereNull('read_at');
if ($request->id) {
$query->where('id', $request->id);
}
$query->update(['read_at' => now()]);
return response()->json(['success' => true]);
}
public function send(User $user, array $data): void
{
$notification = $user->notifications()->create($data);
broadcast(new NotificationCreatedEvent($user->id, $notification))->toOthers();
}
}
Laravel Echo + WebSocket: Client Integration
// hooks/useNotifications.ts
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
const echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_KEY,
cluster: import.meta.env.VITE_PUSHER_CLUSTER,
forceTLS: true,
});
export function useNotifications(userId: number) {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => {
api.get('/api/notifications').then(({ data }) => {
setNotifications(data.notifications);
setUnreadCount(data.unread_count);
});
const channel = echo.private(`notifications.${userId}`)
.listen('.NotificationCreated', (event: { notification: Notification }) => {
setNotifications(prev => [event.notification, ...prev].slice(0, 50));
setUnreadCount(c => c + 1);
if (Notification.permission === 'granted') {
new Notification(event.notification.title ?? 'New notification', {
body: event.notification.body ?? undefined,
icon: '/icon-192.png',
});
}
});
return () => channel.stopListening('.NotificationCreated');
}, [userId]);
const markAllRead = async () => {
await api.post('/api/notifications/mark-read');
setNotifications(prev => prev.map(n => ({ ...n, read_at: new Date().toISOString() })));
setUnreadCount(0);
};
return { notifications, unreadCount, markAllRead };
}
React: UI Component
function NotificationBell({ userId }: { userId: number }) {
const { notifications, unreadCount, markAllRead } = useNotifications(userId);
const [isOpen, setIsOpen] = useState(false);
return (
<div className="notification-bell">
<button
onClick={() => setIsOpen(!isOpen)}
aria-label={`${unreadCount} unread notifications`}
aria-expanded={isOpen}
aria-haspopup="true"
>
🔔
{unreadCount > 0 && (
<span className="badge" aria-hidden>{unreadCount > 99 ? '99+' : unreadCount}</span>
)}
</button>
{isOpen && (
<div className="notification-panel" role="dialog" aria-label="Notifications">
<header>
<h2>Notifications</h2>
{unreadCount > 0 && (
<button onClick={markAllRead}>Mark all read</button>
)}
</header>
<ul>
{notifications.length === 0 && <li className="empty">No notifications</li>}
{notifications.map(notification => (
<li key={notification.id} className={notification.read_at ? 'read' : 'unread'}>
{notification.url ? (
<a href={notification.url}>{notification.title}</a>
) : (
<span>{notification.title}</span>
)}
<time dateTime={notification.created_at}>{timeAgo(notification.created_at)}</time>
{notification.body && <p>{notification.body}</p>}
</li>
))}
</ul>
</div>
)}
</div>
);
}
Work Process and Timelines
How is the stability of real-time notifications guaranteed?
We guarantee 99.9% uptime for the notification center. We use queues (Laravel Queues) with retry logic on failures. On the client — automatic reconnection via Echo (Reconnect). For Pusher — fallback to SSE. For self-hosted — monitoring via Laravel Horizon and Telegram alerts. We have years of experience with complex web applications and over 20 notification center implementations. A transparent process ensures you get a working solution within agreed timelines.
- Analysis — we review requirements, load, and select the WebSocket driver.
- Design — design DB schema, API, events.
- Implementation — write code, test on staging.
- Testing — load tests (10,000+ connections), fallback verification.
- Deployment — set up production environment, monitoring.
What's Included
- Documentation: architecture, DB schema, event sequence, environment setup.
- Code: backend (Laravel), frontend (React), queue and broadcasting configuration.
- Access to repository and CI/CD.
- Team training: 1–2 online meetings.
- Support: 1 month after deployment.
Investment in a notification center pays off through reduced load and increased user loyalty. To get a ready-made solution, contact us — we will audit your project and propose the optimal architecture. Request a consultation with our engineers.
Implementation Timeline
Basic integration: 2–3 days. Self-hosted WebSocket (Soketi/Laravel Reverb): +1 day. If custom grouping or filtering logic is needed, timeline is discussed separately.







