R
Interview Deckreact · next.js · senior/staff
0·0·122
System Design

Push Notifications Web — Service Worker + Web Push

Notificações mesmo com o app fechado. Web Push API, VAPID keys, e gerenciamento de permissão.

Web Push Notifications funcionam via Service Worker + Push API + Notification API. Pipeline: usuário concede permissão → browser gera subscription (endpoint + keys) → backend salva a subscription → backend envia push via Web Push Protocol (FCM/WNS) → Service Worker recebe o push event e exibe a notificação, mesmo com o app fechado. VAPID keys autenticam o servidor no envio.

Problema
Pedir permissão de notificação no primeiro carregamento do app (momento errado, usuário rejeita). Ou enviar notificações sem contexto relevante. Uma vez rejeitada a permissão, só o usuário pode reverter manualmente nas configurações do browser.
Solução
Pedir permissão só quando há valor claro para o usuário: após confirmar uma ação que se beneficia de notificação ('sua corrida foi confirmada — quer ser notificado quando o motorista chegar?'). Guardar a subscription no backend por usuário. Para PWA em iOS (14+): Web Push funciona mas o app precisa ser instalado via Add to Homescreen.
Tip
Service Worker para push precisa ser registrado em /sw.js (ou configurar scope). Next.js: usar next-pwa ou criar /public/sw.js manualmente para o push handler. A subscription tem um endpoint único por browser+dispositivo — um usuário em 3 devices tem 3 subscriptions. Ao notificar, iterar sobre todas as subscriptions do usuário.
// Frontend: solicitar permissão e salvar subscription
async function subscribeToPush() {
  if (!('Notification' in window) || !('serviceWorker' in navigator)) return

  // Primeiro: UI própria com benefício explicado
  const userAccepted = await showCustomPermissionDialog()
  if (!userAccepted) return

  const permission = await Notification.requestPermission()
  if (permission !== 'granted') return

  const registration = await navigator.serviceWorker.ready

  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY
  })

  // Salvar no backend
  await fetch('/api/push-subscription', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription)
  })
}

// Service Worker: receber e exibir
// public/sw.js
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {}
  event.waitUntil(
    self.registration.showNotification(data.title ?? 'Nova notificação', {
      body: data.body,
      icon: '/icon-192.png',
      badge: '/badge-72.png',
      data: { url: data.url },
      actions: [
        { action: 'open', title: 'Ver' },
        { action: 'dismiss', title: 'Ignorar' }
      ]
    })
  )
})

self.addEventListener('notificationclick', (event) => {
  event.notification.close()
  if (event.action === 'open' || !event.action) {
    event.waitUntil(clients.openWindow(event.notification.data.url))
  }
})

// Backend: enviar push (Node.js)
import webpush from 'web-push'
webpush.setVapidDetails('mailto:dev@app.com', VAPID_PUBLIC, VAPID_PRIVATE)

async function notifyUser(userId: string, payload: Notification) {
  const subscriptions = await db.pushSubscriptions.findMany({ where: { userId } })
  await Promise.allSettled(subscriptions.map(async (sub) => {
    try {
      await webpush.sendNotification(sub.data, JSON.stringify(payload))
    } catch (err: any) {
      if (err.statusCode === 410) { // Gone — subscription expirada
        await db.pushSubscriptions.delete({ where: { id: sub.id } })
      }
    }
  }))
}
Q.Como pedir permissão de push de forma não intrusiva?
A.Two-step permission: primeiro mostrar uma UI própria explicando o benefício ('receba alertas quando seu pedido sair para entrega'). Só se o usuário clicar em 'Ativar' na UI sua, chamar Notification.requestPermission(). Se o browser perguntar e o usuário aceitar, registrar a subscription. Não chamar requestPermission() direto sem consentimento explícito — cria bad UX e o usuário rejeita por reflexo.
Q.Como enviar push notification do backend?
A.Usar web-push library (Node.js): `webpush.sendNotification(subscription, JSON.stringify(payload))`. O payload tem: title, body, icon, badge, actions, data (para deep link ao clicar). No Service Worker: `self.addEventListener('push', e => { const data = e.data.json(); self.registration.showNotification(data.title, data) })`. Ao clicar: `self.addEventListener('notificationclick', e => { clients.openWindow(e.notification.data.url) })`.
Q.Como lidar com subscriptions expiradas?
A.O endpoint pode expirar (usuário troca browser, reinstala, limpa dados). Ao tentar enviar: web-push retorna 410 Gone — remover a subscription do banco. Implementar o cleanup no catch do sendNotification. Para garantir subscriptions atualizadas: ao montar o app, comparar a subscription atual do browser com a salva no servidor e atualizar se diferente via PUT /api/push-subscription.
122/122← →