# Migración a Web Push Nativo (VAPID) **Fecha:** 2025-11-29 **Versión:** 2.0 **Autor:** Backend-Agent ## Resumen Se migró el sistema de push notifications de **Firebase Cloud Messaging (FCM)** a **Web Push API nativo** usando la librería `web-push` y el protocolo VAPID (Voluntary Application Server Identification). ## Motivación ### Problemas con Firebase: - Dependencia de servicios externos (vendor lock-in) - Requiere configuración de cuenta Firebase - Complejidad en credenciales (service account JSON) - Costos potenciales en producción - Latencia adicional por proxy de Firebase ### Ventajas de Web Push nativo: - Sin dependencias de servicios externos - Claves VAPID generadas localmente - Protocolo estándar W3C - Compatible con todos los navegadores modernos - Control total sobre la infraestructura - Sin costos adicionales ## Cambios Implementados ### 1. Dependencias **Eliminado:** ```bash npm uninstall firebase-admin ``` **Agregado:** ```bash npm install web-push ``` ### 2. Servicio Principal **Archivo:** `src/modules/notifications/services/push-notification.service.ts` **Cambios principales:** - Reemplazado `firebase-admin` por `web-push` - Cambio de autenticación: Service Account → VAPID Keys - Cambio de formato de token: FCM Token → PushSubscription JSON - Manejo de errores adaptado: FCM codes → HTTP status codes **Antes (Firebase):** ```typescript import * as admin from 'firebase-admin'; // Inicialización admin.initializeApp({ credential: admin.credential.cert({ projectId: 'xxx', clientEmail: 'xxx', privateKey: 'xxx' }) }); // Envío await admin.messaging().send({ token: 'fcm-token', notification: { title, body } }); ``` **Después (Web Push):** ```typescript import * as webpush from 'web-push'; // Inicialización webpush.setVapidDetails( 'mailto:admin@gamilit.com', VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY ); // Envío const subscription = JSON.parse(deviceToken); await webpush.sendNotification(subscription, payload); ``` ### 3. Controller **Archivo:** `src/modules/notifications/controllers/notification-devices.controller.ts` **Nuevo endpoint agregado:** ```typescript @Get('vapid-public-key') getVapidPublicKey() { const key = this.pushNotificationService.getVapidPublicKey(); return { vapidPublicKey: key }; } ``` Este endpoint NO requiere autenticación porque la clave pública es necesaria ANTES de que el usuario se autentique (para crear la subscription). ### 4. Variables de Entorno **Archivo:** `.env.production.example` **Eliminado:** ```bash FIREBASE_PROJECT_ID=xxx FIREBASE_CLIENT_EMAIL=xxx FIREBASE_PRIVATE_KEY=xxx ``` **Agregado:** ```bash VAPID_PUBLIC_KEY= VAPID_PRIVATE_KEY= VAPID_SUBJECT=mailto:admin@gamilit.com ``` ### 5. Generación de Claves VAPID **Script creado:** `scripts/generate-vapid-keys.js` **Uso:** ```bash npm run generate:vapid ``` **Output:** ``` VAPID_PUBLIC_KEY=BN4GvZtEZiZuqaaObWga7lEP-S1WCv7L1c... VAPID_PRIVATE_KEY=aB3cDefGh4IjKlM5nOpQr6StUvWxYz... VAPID_SUBJECT=mailto:admin@gamilit.com ``` ### 6. Módulo de Notificaciones **Archivo:** `src/modules/notifications/notifications.module.ts` - Actualizada documentación del módulo - `PushNotificationService` ahora usa `web-push` en lugar de Firebase - No se requirieron cambios en providers/exports ## Estructura de Datos ### Antes: FCM Token ``` deviceToken: "dUzV1qzxTHGKj8qY9ZxYzP:APA91bF..." ``` ### Después: PushSubscription JSON ```json { "endpoint": "https://fcm.googleapis.com/fcm/send/...", "keys": { "p256dh": "BN4GvZtEZiZuqaaObWga7lEP...", "auth": "aB3cDefGh4IjKlM5nOpQr6StUvWxYz..." } } ``` **Nota:** El `endpoint` puede ser de FCM, Mozilla Push Service, Windows Push Service, etc. El protocolo Web Push es universal. ## Configuración en Producción ### 1. Generar Claves VAPID ```bash cd apps/backend npm run generate:vapid ``` ### 2. Copiar Variables Agregar las claves generadas a `.env.production`: ```bash # Web Push VAPID Keys VAPID_PUBLIC_KEY= VAPID_PRIVATE_KEY= VAPID_SUBJECT=mailto:admin@gamilit.com ``` ### 3. Seguridad - ❌ NUNCA commitear `VAPID_PRIVATE_KEY` al repositorio - ✅ Usar variables de entorno del sistema en producción - ✅ Restringir permisos de archivo `.env.production` (chmod 600) ### 4. Reiniciar Backend ```bash npm run build npm run prod ``` ## Integración Frontend ### 1. Obtener Clave Pública VAPID ```typescript const response = await fetch('/api/notifications/devices/vapid-public-key'); const { vapidPublicKey } = await response.json(); ``` ### 2. Crear PushSubscription ```typescript // Verificar soporte if ('serviceWorker' in navigator && 'PushManager' in window) { // Registrar Service Worker const registration = await navigator.serviceWorker.register('/sw.js'); // Solicitar permiso const permission = await Notification.requestPermission(); if (permission === 'granted') { // Crear subscription const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) }); // Enviar a backend await fetch('/api/notifications/devices', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ deviceToken: JSON.stringify(subscription), deviceType: 'web', deviceName: navigator.userAgent }) }); } } ``` ### 3. Utility Function ```typescript function urlBase64ToUint8Array(base64String: string): Uint8Array { 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; } ``` ### 4. Service Worker **Archivo:** `public/sw.js` ```javascript self.addEventListener('push', event => { const data = event.data.json(); const options = { body: data.body, icon: data.icon || '/icons/icon-192x192.png', badge: data.badge || '/icons/badge-72x72.png', vibrate: data.vibrate || [200, 100, 200], data: data.data || {}, tag: data.tag || 'gamilit-notification' }; event.waitUntil( self.registration.showNotification(data.title, options) ); }); self.addEventListener('notificationclick', event => { event.notification.close(); const url = event.notification.data?.url || '/'; event.waitUntil( clients.openWindow(url) ); }); ``` ## Compatibilidad de Navegadores | Navegador | Versión Mínima | Notas | |-----------|---------------|-------| | Chrome | 42+ | Full support | | Firefox | 44+ | Full support | | Edge | 17+ | Full support | | Safari | 16.4+ | iOS 16.4+ required | | Opera | 29+ | Full support | | Samsung Internet | 4+ | Full support | ## Manejo de Errores ### HTTP Status Codes | Código | Significado | Acción | |--------|-------------|--------| | 201 | Created | Notification enviada exitosamente | | 410 | Gone | Subscription expirada, invalidar device | | 404 | Not Found | Subscription no encontrada, invalidar device | | 400 | Bad Request | Subscription malformada | ### Invalidación Automática El servicio invalida automáticamente subscriptions expiradas: ```typescript // En push-notification.service.ts if (error.statusCode === 410 || error.statusCode === 404) { throw new InvalidSubscriptionError(error.statusCode); } // En sendToUser() if (error instanceof InvalidSubscriptionError) { await this.userDeviceService.invalidateDevice(userId, deviceToken); } ``` ## Testing ### 1. Verificar Inicialización ```bash # Logs al iniciar backend [PushNotificationService] Web Push initialized successfully with VAPID keys ``` ### 2. Obtener Clave Pública ```bash curl http://localhost:3006/api/notifications/devices/vapid-public-key ``` **Response esperado:** ```json { "vapidPublicKey": "BN4GvZtEZiZuqaaObWga7lEP..." } ``` ### 3. Enviar Notificación de Prueba ```typescript // En cualquier service await this.pushNotificationService.sendToUser(userId, { title: 'Test Notification', body: 'This is a test from Web Push', icon: '/icons/icon-192x192.png', data: { url: '/dashboard' } }); ``` ## Troubleshooting ### Problema: "VAPID keys not configured" **Causa:** Variables de entorno no configuradas **Solución:** ```bash npm run generate:vapid # Copiar output a .env ``` ### Problema: "Invalid device token format" **Causa:** deviceToken no es JSON válido de PushSubscription **Solución:** - Verificar que frontend envía `JSON.stringify(subscription)` - Verificar que no hay corrupción en base de datos ### Problema: "Subscription expired (status: 410)" **Causa:** Usuario revocó permisos o desinstaló app **Solución:** - El servicio invalida automáticamente - Frontend debe re-registrar subscription ## Rollback (si es necesario) Si se necesita volver a Firebase: 1. Reinstalar firebase-admin: ```bash npm install firebase-admin ``` 2. Restaurar servicio desde git: ```bash git checkout HEAD~1 -- src/modules/notifications/services/push-notification.service.ts ``` 3. Restaurar variables de entorno: ```bash # Usar FIREBASE_* en lugar de VAPID_* ``` ## Referencias - [Web Push Protocol Spec](https://datatracker.ietf.org/doc/html/rfc8030) - [VAPID Spec](https://datatracker.ietf.org/doc/html/rfc8292) - [MDN Web Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) - [web-push npm package](https://www.npmjs.com/package/web-push) - [Push Notifications Guide](https://web.dev/push-notifications-overview/) ## Checklist de Migración ### Backend - [x] Desinstalar firebase-admin - [x] Instalar web-push - [x] Reescribir PushNotificationService - [x] Agregar endpoint VAPID public key - [x] Actualizar .env.production.example - [x] Crear script generate-vapid-keys.js - [x] Ejecutar npm run build (sin errores) - [x] Ejecutar npm run lint (sin errores) ### Frontend (Pendiente) - [ ] Obtener VAPID public key desde API - [ ] Crear PushSubscription con VAPID key - [ ] Registrar Service Worker - [ ] Implementar utility urlBase64ToUint8Array - [ ] Enviar subscription (JSON) a backend - [ ] Manejar notificationclick event ### Producción (Pendiente) - [ ] Generar claves VAPID para producción - [ ] Configurar variables de entorno - [ ] Deploy backend actualizado - [ ] Probar push notifications en navegadores - [ ] Monitorear logs de errores ## Notas Finales Esta migración elimina la dependencia de Firebase manteniendo toda la funcionalidad de push notifications. El sistema ahora es: - Más simple (menos configuración) - Más económico (sin costos de Firebase) - Más portable (estándar W3C) - Más controlable (infraestructura propia) La única consideración es que Safari requiere iOS 16.4+ (lanzado marzo 2023), pero la mayoría de usuarios ya tienen esta versión o superior.