Real-Time Backend

In-App Notifications

Slack показывает непрочитанные сообщения на всех устройствах синхронно за < 100ms. Как работает система in-app уведомлений за этим?

  • **Slack** - WebSocket для real-time доставки, badge count синхронизируется между desktop/mobile/web за <100ms через Redis Pub/Sub, прочитанные на одном устройстве обновляются на всех остальных
  • **GitHub** - SSE (Server-Sent Events) для notification bell, cursor-based пагинация с группировкой ('3 новых комментария к Issue #456'), partial index на read_at IS NULL для быстрого count
  • **Linear** - notification grouping снизил количество отдельных уведомлений на 60%, preference center с гранулярным управлением по типам событий и каналам доставки

In-App Delivery

**In-app notification** - уведомление, которое доставляется пока пользователь находится в приложении. В отличие от push-уведомлений (работают когда приложение закрыто), in-app система работает через открытое соединение: WebSocket, SSE (Server-Sent Events) или long polling. Пользователь видит notification bell с числом и dropdown-список событий.

Архитектура: notification service публикует событие в Redis Pub/Sub или Kafka. Горизонтально-масштабируемый WebSocket сервер подписан на события и форвардирует их в нужный WebSocket канал. Ключевая проблема: пользователь может быть подключён к любой из N нод - нужно знать, на какой ноде его соединение.

GitHub использует SSE для real-time уведомлений в web-интерфейсе - счётчик непрочитанных обновляется без перезагрузки страницы. SSE проще WebSocket для однонаправленной доставки: браузер поддерживает нативно, автоматический reconnect встроен в EventSource API.

Пользователь подключён к WebSocket ноде #3. Notification Service работает на ноде #1. Как уведомление доберётся до пользователя?

Badge Count

**Badge count** - счётчик непрочитанных уведомлений (красный кружок на иконке приложения на iOS, или число в navigation bar). Кажется простым, но при нескольких устройствах и горизонтальном масштабировании становится нетривиальным: пользователь прочитал уведомления на iPad - счётчик на iPhone тоже должен обнулиться.

Паттерн: счётчик хранится в БД (PostgreSQL: `SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND read = false`). При любом изменении (новое уведомление, отметить прочитанным) - событие публикуется в Redis, все устройства пользователя через WebSocket получают обновлённый счётчик. APNs badge обновляется отдельным silent push.

Slack синхронизирует badge count между desktop, mobile и web в реальном времени. Алгоритм: при отметке 'прочитано' на одном устройстве - Slack сервер пересчитывает count и рассылает обновление всем активным сессиям пользователя через WebSocket. Задержка синхронизации - менее 100ms.

Почему badge count хранится в PostgreSQL, а не только в памяти WebSocket сервера?

Read/Unread State

Управление состоянием прочитано/непрочитано - это не просто `UPDATE notifications SET read = true`. Нужно учитывать: **partial reads** (пользователь видел notification bell, но не открыл конкретное уведомление), **bulk mark-as-read** (нажал «отметить все прочитанными»), **optimistic updates** (UI обновляется сразу, запрос идёт в фоне).

Схема БД с timestamp вместо boolean даёт больше аналитики и поддерживает частичные состояния. Bulk mark-as-read эффективнее через один UPDATE с timestamp, чем N отдельных запросов. Для аналитики read rate - важная метрика качества уведомлений.

Notion разделяет «видел» и «прочитал»: notification bell со счётчиком обновляется при открытии dropdown (seen_at), но уведомление остаётся непрочитанным пока не кликнул. Это увеличивает click-through rate на 23% по их внутренним данным - пользователи не теряют важные уведомления.

Чем `read_at TIMESTAMPTZ` лучше `is_read BOOLEAN` для хранения состояния уведомления?

Notification UI и UX

Notification dropdown - компонент с требованиями к производительности: пагинация (не загружать все 500 уведомлений сразу), infinite scroll или «показать ещё», группировка по типу или дате. Первый экран должен загрузиться за < 200ms - значит хранить в БД с индексами, а не вычислять на лету.

**Optimistic updates**: при клике на уведомление сразу показываем его как прочитанное (убираем выделение), API запрос идёт в фоне. Если запрос упал - rollback через state management. Это делает UI отзывчивым даже при плохой сети. GitHub и Linear используют этот паттерн для notification читалки.

  • **Пагинация**: `LIMIT 20 OFFSET 0` с cursor-based пагинацией для стабильных результатов при новых уведомлениях
  • **Группировка**: уведомления одного типа за 24ч схлопываются - '5 человек лайкнули ваш пост' вместо 5 отдельных
  • **Real-time**: новые уведомления появляются в dropdown через WebSocket без reload страницы
  • **Preference center**: пользователь выбирает какие типы уведомлений получать и по каким каналам (push/email/in-app)

Linear (project management tool) публиковала: notification grouping снизил noise (число отдельных уведомлений) на 60%. Вместо 20 уведомлений о комментариях к одной задаче - одно «Alex и ещё 4 прокомментировали Issue #123».

In-app notifications = просто показать список из БД в dropdown

In-app notifications - система из нескольких слоёв: real-time WebSocket delivery, badge sync между устройствами, read/seen state, optimistic UI, группировка, preference center

Без real-time delivery пользователь не видит новое уведомление без reload. Без badge sync - неправильный счётчик на iPhone после чтения на iPad. Без группировки - notification fatigue и отключение уведомлений.

Зачем использовать cursor-based пагинацию вместо OFFSET для списка уведомлений?

Итоги

  • **Real-time delivery** через WebSocket + Redis Pub/Sub: событие публикуется в канал пользователя, любая WebSocket нода форвардирует в его соединение
  • **Badge count** - PostgreSQL как source of truth, Redis для broadcast обновлений на все устройства; `read_at TIMESTAMPTZ` вместо boolean для аналитики и seen/read разделения
  • **UI patterns**: optimistic updates для отзывчивости, cursor-based пагинация для стабильности, grouping для снижения noise - каждый слой влияет на retention

Связанные темы

In-app notifications строятся поверх реального времени и интегрируются с push:

  • Push Notifications — Push - дополняет in-app: когда приложение закрыто - APNs/FCM, когда открыто - WebSocket in-app канал
  • WebSocket — Транспортный слой для real-time доставки уведомлений внутри приложения
  • Redis Pub/Sub — Message bus между notification service и WebSocket нодами для масштабируемого fan-out

Вопросы для размышления

  • У пользователя 3 устройства (iPhone, iPad, MacBook). На MacBook прочитал 5 уведомлений. Описать полный flow синхронизации badge count на iPhone и iPad с точностью до компонентов.
  • Notification grouping: 100 пользователей лайкнули пост за 5 минут. Как сгруппировать их в одно уведомление 'Alex и ещё 99 лайкнули ваш пост' на уровне БД-запроса и WebSocket события?
  • Preference center позволяет отключить email, но оставить in-app. Как хранить настройки и применять их в notification pipeline без дублирования логики в каждом сервисе?

Связанные уроки

  • net-01-intro
In-App Notifications

0

1

Войти