Push Notifications Web — Service Worker + Web Push
Notificações mesmo com o app fechado. Web Push API, VAPID keys, e gerenciamento de permissão.
Definiçã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 → Solução
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.Dica de entrevista
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.Código
// 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 } })
}
}
}))
}Perguntas de entrevista
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.
Termos relacionados
122/122← →