- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
11 KiB
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:
npm uninstall firebase-admin
Agregado:
npm install web-push
2. Servicio Principal
Archivo: src/modules/notifications/services/push-notification.service.ts
Cambios principales:
- Reemplazado
firebase-adminporweb-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):
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):
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:
@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:
FIREBASE_PROJECT_ID=xxx
FIREBASE_CLIENT_EMAIL=xxx
FIREBASE_PRIVATE_KEY=xxx
Agregado:
VAPID_PUBLIC_KEY=<generar>
VAPID_PRIVATE_KEY=<generar>
VAPID_SUBJECT=mailto:admin@gamilit.com
5. Generación de Claves VAPID
Script creado: scripts/generate-vapid-keys.js
Uso:
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
PushNotificationServiceahora usaweb-pushen lugar de Firebase- No se requirieron cambios en providers/exports
Estructura de Datos
Antes: FCM Token
deviceToken: "dUzV1qzxTHGKj8qY9ZxYzP:APA91bF..."
Después: PushSubscription 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
cd apps/backend
npm run generate:vapid
2. Copiar Variables
Agregar las claves generadas a .env.production:
# Web Push VAPID Keys
VAPID_PUBLIC_KEY=<clave-publica-generada>
VAPID_PRIVATE_KEY=<clave-privada-generada>
VAPID_SUBJECT=mailto:admin@gamilit.com
3. Seguridad
- ❌ NUNCA commitear
VAPID_PRIVATE_KEYal repositorio - ✅ Usar variables de entorno del sistema en producción
- ✅ Restringir permisos de archivo
.env.production(chmod 600)
4. Reiniciar Backend
npm run build
npm run prod
Integración Frontend
1. Obtener Clave Pública VAPID
const response = await fetch('/api/notifications/devices/vapid-public-key');
const { vapidPublicKey } = await response.json();
2. Crear PushSubscription
// 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
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
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:
// 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
# Logs al iniciar backend
[PushNotificationService] Web Push initialized successfully with VAPID keys
2. Obtener Clave Pública
curl http://localhost:3006/api/notifications/devices/vapid-public-key
Response esperado:
{
"vapidPublicKey": "BN4GvZtEZiZuqaaObWga7lEP..."
}
3. Enviar Notificación de Prueba
// 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:
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:
- Reinstalar firebase-admin:
npm install firebase-admin
- Restaurar servicio desde git:
git checkout HEAD~1 -- src/modules/notifications/services/push-notification.service.ts
- Restaurar variables de entorno:
# Usar FIREBASE_* en lugar de VAPID_*
Referencias
Checklist de Migración
Backend
- Desinstalar firebase-admin
- Instalar web-push
- Reescribir PushNotificationService
- Agregar endpoint VAPID public key
- Actualizar .env.production.example
- Crear script generate-vapid-keys.js
- Ejecutar npm run build (sin errores)
- 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.