Push-уведомления

В этом модуле добавим Web Push уведомления — они будут приходить, когда пользователь офлайн.

Перед началом: Модуль 5. Инструменты: Web Push API, VAPID. Время: ~4 ч.


🔥 Инцидент. Пользователь закрыл вкладку. Приходит новое сообщение — он не знает. Push-уведомление должно прийти на телефон/десктоп. Но: Safari требует разных ключей, Firefox — других, Chrome — third-party cookies... Push API — стандартизирован, но полон подводных камней.


📖 Теория

Фокус: Web Push через VAPID. Сервер шлёт зашифрованный payload push-сервису браузера, Service Worker показывает уведомление даже при закрытой вкладке.

Web Push API и VAPID

Web Push позволяет отправлять уведомления пользователю даже когда браузер закрыт.

VAPID (Voluntary Application Server Identification) — это механизм аутентификации, который позволяет браузеру доверять вашему серверу.

Генерация VAPID ключей

bash
npm install -g web-push
web-push generate-vapid-keys

Сохраните public key на фронтенде, private key — на бэкенде.

Запрос разрешения и подписка (Frontend)

typescript
// utils/pushNotifications.ts
export async function subscribeUserToPush(vapidPublicKey: string) {
  if ('serviceWorker' in navigator) {
    const registration = await navigator.serviceWorker.ready;

    // Запрос разрешения
    const permission = await Notification.requestPermission();
    if (permission !== 'granted') return;

    // Подписка на пуши
    const subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
    });

    // Отправляем подписку на бэкенд
    await fetch('/api/subscribe', {
      method: 'POST',
      body: JSON.stringify(subscription),
      headers: { 'Content-Type': 'application/json' }
    });
  }
}

function urlBase64ToUint8Array(base64String: string) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

Service Worker (обработка пуша)

typescript
// sw.ts
self.addEventListener('push', (event) => {
  const data = event.data?.json() || {
    title: 'Новое сообщение',
    body: 'У вас новое сообщение в чате'
  };

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icon-192.png',
      badge: '/badge-72.png',
      data: { url: '/' } // URL для перехода при клике
    })
  );
});

// Обработка клика на уведомление
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  event.waitUntil(
    clients.openWindow(event.notification.data.url || '/')
  );
});

Отправка пуша (Backend, Go)

Используем библиотеку github.com/SherClockHolmes/webpush-go:

go
func SendPush(subscription webpush.Subscription, message string) error {
  resp, err := webpush.SendNotification([]byte(message), &subscription, &webpush.Options{
    Subscriber:      "admin@example.com",
    VAPIDPublicKey:  "YOUR_PUBLIC_KEY",
    VAPIDPrivateKey: "YOUR_PRIVATE_KEY",
    TTL:             30,
  })
  if err != nil {
    return err
  }
  defer resp.Body.Close()
  return nil
}

Lambda Function для отправки пушей

go
// cmd/sendpush/main.go
func handler(ctx context.Context, event Event) (*Response, error) {
  // 1. Получаем подписки из YDB для получателя
  subscriptions, _ := getSubscriptions(event.UserID)

  // 2. Отправляем пуш каждому
  for _, sub := range subscriptions {
    SendPush(sub, event.Message)
  }

  return &Response{StatusCode: 200}, nil
}

В следующем модуле задеплоим наше приложение.


💱 Компромиссы (Trade-offs)

РешениеПлюсыМинусы
VAPID (public/private keys)Стандарт аутентификации сервера у push-сервисаКлючи надо хранить, ротация = переподписка
Push-subscription в БДСервер знает, куда слать пушИстекает при смене браузера/устройства
aes128gcm payloadСквозное шифрование, приватностьЖёсткий лимит размера (≤ 4 КБ)
Уведомление через SWРаботает при закрытой вкладкеSafari/Firefox/Chrome — разные quirks
TTL на пушСообщение дойдёт после оффлайна (в срок TTL)Старые пуши могут прийти «пачкой»

🧩 Проверь себя

1. Push-payload должен быть...
2. VAPID — это...
3. Push-subscription истекает, когда...
4. Push-сервис браузера (FCM/autopush/APNs) — для чего?
5. Web Push требует...

🔗 Мостик к Модулю 7

Push работает. Покроем всё тестами.