Реалізація real-time центру сповіщень
Уявіть: ваш SaaS-продукт обробляє сотні подій за хвилину — нові замовлення, коментарі, статуси завдань. Користувачі скаржаться, що пропускають важливі сповіщення, а адмінка перевантажена. На одному з проєктів 20 000 користувачів одночасно генерували 500 сповіщень за секунду, і без правильної архітектури система падала. Після впровадження нашого рішення навантаження на сервер знизилося на 70%. Ми вирішили цю проблему, впровадивши real-time центр сповіщень на зв'язці Laravel + WebSocket + React. Під капотом — оптимізована модель БД, асинхронна доставка та продуманий UX.
Проблеми, які вирішує центр сповіщень
Традиційні рішення (polling) створюють надмірне навантаження на сервер: кожні 5 секунд — запит до БД. При 1000 активних користувачах це 200 RPS тільки на перевірку сповіщень. WebSocket (Laravel Echo + Pusher) дає push-модель: сповіщення приходить миттєво, без зайвих запитів. Це знижує TTFB у 3–4 рази та покращує Core Web Vitals. Наприклад, після впровадження на проєкті з 5000 DAU кількість скарг на затримки сповіщень скоротилася на 90%. Інша проблема — втрата сповіщень через невірну архітектуру: клієнт може не отримати подію, якщо WebSocket-з'єднання розірвано. Наше рішення використовує черги Laravel з автоматичним повторенням та механізмом підтвердження доставки.
Вибір драйвера WebSocket
| Драйвер | Хостинг | Масштабування | Затримка | Особливості |
|---|---|---|---|---|
| Pusher | SaaS | Вбудовано | <20ms | Швидкий старт, фіксована щомісячна плата |
| Soketi | Self-hosted | Горизонтальне через Redis | <50ms | Безкоштовно, повний контроль |
| Laravel Reverb | Self-hosted | Горизонтальне через Redis | <30ms | Опенсорс, сумісність з Laravel |
Pusher кращий для швидкого старту, Soketi та Reverb — для повного контролю та економії при великій кількості користувачів. Вибір залежить від вимог до SLA: SaaS дає передбачувану вартість, self-hosted — нижчі витрати на інфраструктуру. Для невеликих проєктів достатньо Pusher, а для enterprise-систем з десятками тисяч коннектів краще підходить Soketi.
Laravel Broadcasting забезпечує push-сповіщення в реальному часі через WebSocket, що знижує затримку до 50 мс.
| Тип сповіщення | Опис | Приклад даних |
|---|---|---|
| Системне | Помилка, попередження | 'У вас закінчилися ліцензії' |
| Соціальне | Лайки, коментарі | 'Користувач X залишив коментар' |
| Бізнес-подія | Замовлення, оплата | 'Нове замовлення №123' |
Структура БД та індекси
Чому важливі індекси в таблиці сповіщень?
Без індексів запити на вибірку непрочитаних сповіщень будуть сканувати всю таблицю. На таблиці з мільйонами рядків це призводить до тайм-аутів. Ми використовуємо покриваючий індекс по (user_id, read_at, created_at DESC), який прискорює сортування та фільтрацію в 10 разів. Розмір таблиці сповіщень може сягати 10 мільйонів рядків, і без індексів запити виконуються за 5–10 секунд замість 50 мс. Така структура забезпечує швидку вставку та вибірку, а індекс покриває 95% запитів.
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);
Додатково можна додати індекс по type для фільтрації за категоріями.
Laravel: API ендпоінти та 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: клієнтська інтеграція
// 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 ?? 'Нове сповіщення', {
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 компонент
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} непрочитаних сповіщень`}
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="Сповіщення">
<header>
<h2>Сповіщення</h2>
{unreadCount > 0 && (
<button onClick={markAllRead}>Прочитати всі</button>
)}
</header>
<ul>
{notifications.length === 0 && <li className="empty">Немає сповіщень</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>
);
}
Процес роботи та терміни
Як гарантується стабільність real-time сповіщень?
Гарантуємо uptime 99.9% для центру сповіщень. Для цього використовуємо черги (Laravel Queues) з retry-логікою при збоях. На клієнті — автоматичне перепідключення через Echo (Reconnect). Для Pusher — fallback на SSE. Self-hosted — моніторинг через Laravel Horizon та алерти в Telegram. Багаторічний досвід роботи зі складними web-додатками, понад 20 впроваджень центрів сповіщень. Прозорий процес гарантує, що ви отримаєте працююче рішення в обумовлені терміни.
- Аналітика — розбираємо вимоги, навантаження, вибираємо драйвер WebSocket.
- Проектування — проектуємо схему БД, API, events.
- Реалізація — пишемо код, тестуємо в staging.
- Тестування — навантажувальні тести (10 000+ коннектів), перевірка fallback.
- Деплой — налаштування production-оточення, моніторинг.
Що входить в роботу
- Документація: архітектура, схема БД, послідовність подій, налаштування оточення.
- Код: backend (Laravel), frontend (React), конфігурація черг і broadcasting.
- Доступи до репозиторію та CI/CD.
- Навчання команди: 1–2 онлайн-зустрічі.
- Підтримка: 1 місяць після впровадження.
Інвестиції в центр сповіщень окупаються за рахунок зниження навантаження та лояльності користувачів. Щоб отримати готове рішення, зв'яжіться з нами — ми проведемо аудит вашого проєкту і запропонуємо оптимальну архітектуру. Замовте консультацію наших інженерів. Отримайте консультацію інженера.
Термін реалізації
Базова інтеграція: 2–3 дні. Self-hosted WebSocket (Soketi/Laravel Reverb): +1 день. Якщо потрібна кастомна логіка групування або фільтрації — термін обговорюється окремо.







