--- id: "ET-SAAS-007" title: "Especificacion Tecnica Notifications v2" type: "TechnicalSpec" status: "Published" priority: "P0" module: "notifications" version: "2.0.0" created_date: "2026-01-08" updated_date: "2026-01-10" --- # ET-SAAS-007: Especificacion Tecnica - Sistema de Notificaciones v2.0 ## Metadata - **Codigo:** ET-SAAS-007 - **Modulo:** Notifications - **Version:** 2.0.0 - **Estado:** Propuesto - **Fecha:** 2026-01-07 - **Basado en:** gamilit EXT-003, mejores practicas Node.js 2025 --- ## 1. Resumen Ejecutivo ### 1.1 Estado Actual (v1.0) El modulo de notificaciones actual en template-saas incluye: | Componente | Estado | Limitaciones | |------------|--------|--------------| | Email transaccional | Implementado | Envio sincrono, sin cola | | In-app notifications | Basico | Sin WebSocket real-time | | Push notifications | TODO | No implementado | | SMS | Excluido | Fase posterior | | Templates | Implementado | 6 templates predefinidos | | Preferencias usuario | Basico | Sin granularidad por tipo | | Cola de procesamiento | No | Envios sincronos | | Reintentos automaticos | No | Sin retry mechanism | | Logs de entrega | No | Sin tracking detallado | ### 1.2 Propuesta v2.0 Sistema de notificaciones enterprise-ready con: - **Multi-canal**: Email, Push (Web Push API), In-App, SMS (futuro) - **Arquitectura asincrona**: BullMQ para procesamiento en cola - **WebSocket real-time**: Gateway para notificaciones in-app instantaneas - **Push nativo**: Web Push API con VAPID (sin Firebase) - **Preferencias granulares**: Por tipo de notificacion y canal - **Tracking completo**: Logs de entrega con metricas - **Reintentos inteligentes**: Exponential backoff con max attempts - **Multi-tenant**: RLS + aislamiento por tenant_id --- ## 2. Analisis Comparativo ### 2.1 Referencia: gamilit EXT-003 | Caracteristica | gamilit | template-saas actual | Propuesta v2.0 | |----------------|---------|---------------------|----------------| | Tablas DDL | 6 | 3 | 6 | | Push Web (VAPID) | Si | No | Si | | Cola BullMQ | Si | Solo webhooks | Si | | WebSocket Gateway | Si | No | Si | | Devices table | Si | No | Si | | Notification queue | Si | No | Si | | Notification logs | Si | No | Si | | Funciones SQL | 3 | 0 | 3 | | Preferencias/tipo | Si | No | Si | ### 2.2 Mejores Practicas Node.js 2025 Segun investigacion de fuentes como [Novu](https://novu.co/), [NotifMe SDK](https://github.com/notifme/notifme-sdk), y articulos especializados: 1. **Arquitectura desacoplada**: Separar logica de negocio de envios 2. **Event-driven**: Usar event emitters para triggers 3. **Queue-based**: Nunca enviar en el request principal 4. **Channel-agnostic**: Definir notificacion base, mapear a canales 5. **Fallback strategies**: Multi-proveedor con failover 6. **Rate limiting**: Prevenir spam y sobrecarga 7. **Batching**: Digest mode para reducir interrupciones --- ## 3. Arquitectura Propuesta ### 3.1 Diagrama de Componentes ``` +------------------+ +-------------------+ +------------------+ | Event Source |---->| NotificationSvc |---->| BullMQ Queue | | (Auth, Billing, | | (orchestration) | | (notifications) | | Tenant, etc) | +-------------------+ +------------------+ +------------------+ | | v v +-------------------+ +-------------------+ | Template Service | | Queue Processor | | (render vars) | | (worker) | +-------------------+ +-------------------+ | +-----------------------------+-----------------------------+ | | | v v v +-------------------+ +-------------------+ +-------------------+ | Email Channel | | Push Channel | | In-App Channel | | (SendGrid/SES) | | (Web Push API) | | (WebSocket) | +-------------------+ +-------------------+ +-------------------+ | | | v v v +-------------------------------------------------------------------+ | Notification Logs | | (tracking & analytics) | +-------------------------------------------------------------------+ ``` ### 3.2 Flujo de Notificacion ``` 1. Evento dispara notificacion (ej: usuario creado) | 2. NotificationService.send(userId, templateCode, data) | 3. Cargar template + verificar preferencias usuario | 4. Filtrar canales segun preferencias | 5. Por cada canal habilitado: |-- Encolar en BullMQ (job por canal) | 6. Worker procesa job: |-- Email: EmailService.send() |-- Push: PushService.sendToDevices() |-- In-App: WebSocketGateway.emit() + crear registro | 7. Actualizar status en notification_queue | 8. Crear log en notification_logs | 9. Si falla: retry con backoff exponencial ``` --- ## 4. Modelo de Datos (DDL v2.0) ### 4.1 Schema: notifications #### 4.1.1 Enums Adicionales ```sql -- Ya existentes en 02-enums.sql: -- notifications.channel: 'email', 'push', 'in_app', 'sms', 'whatsapp' -- notifications.notification_status: 'pending', 'sent', 'delivered', 'failed', 'read' -- notifications.priority: 'low', 'normal', 'high', 'urgent' -- Nuevos enums: CREATE TYPE notifications.queue_status AS ENUM ( 'queued', -- En cola esperando 'processing', -- Siendo procesado 'sent', -- Enviado exitosamente 'failed', -- Fallo definitivo 'retrying' -- En espera de retry ); CREATE TYPE notifications.device_type AS ENUM ( 'web', 'mobile', 'desktop' ); ``` #### 4.1.2 Tablas (6 total) **1. notifications.templates** (existente - sin cambios) **2. notifications.notifications** (existente - sin cambios) **3. notifications.user_preferences** (mejorada) ```sql -- Agregar campos para preferencias granulares por tipo ALTER TABLE notifications.user_preferences ADD COLUMN IF NOT EXISTS notification_type_preferences JSONB DEFAULT '{}'::jsonb; -- Estructura: { "achievement": {"email": true, "push": true}, "billing": {"email": true, "push": false} } COMMENT ON COLUMN notifications.user_preferences.notification_type_preferences IS 'Preferencias granulares por tipo de notificacion y canal'; ``` **4. notifications.user_devices** (NUEVA) ```sql CREATE TABLE notifications.user_devices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, -- Device info device_type notifications.device_type NOT NULL DEFAULT 'web', device_token TEXT NOT NULL, -- PushSubscription JSON para Web Push device_name VARCHAR(100), -- "Chrome en Windows", "Safari en macOS" -- Browser/OS info browser VARCHAR(50), browser_version VARCHAR(20), os VARCHAR(50), os_version VARCHAR(20), -- Status is_active BOOLEAN DEFAULT TRUE, -- Timestamps last_used_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, -- Constraints CONSTRAINT unique_user_device_token UNIQUE (user_id, device_token) ); -- Indexes CREATE INDEX idx_user_devices_user ON notifications.user_devices(user_id) WHERE is_active = TRUE; CREATE INDEX idx_user_devices_tenant ON notifications.user_devices(tenant_id); -- RLS ALTER TABLE notifications.user_devices ENABLE ROW LEVEL SECURITY; CREATE POLICY user_devices_tenant_isolation ON notifications.user_devices USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); COMMENT ON TABLE notifications.user_devices IS 'Dispositivos registrados para push notifications'; ``` **5. notifications.notification_queue** (NUEVA) ```sql CREATE TABLE notifications.notification_queue ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), notification_id UUID NOT NULL REFERENCES notifications.notifications(id) ON DELETE CASCADE, -- Channel channel notifications.channel NOT NULL, -- Scheduling scheduled_for TIMESTAMPTZ DEFAULT NOW(), -- Priority (numerico para ordenamiento) priority_value INT DEFAULT 0, -- urgent=10, high=5, normal=0, low=-5 -- Processing attempts INT DEFAULT 0, max_attempts INT DEFAULT 3, status notifications.queue_status DEFAULT 'queued', -- Timestamps last_attempt_at TIMESTAMPTZ, next_retry_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, -- Error tracking error_message TEXT, error_count INT DEFAULT 0, -- Audit created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL ); -- Indexes para procesamiento eficiente CREATE INDEX idx_queue_pending ON notifications.notification_queue(scheduled_for, priority_value DESC) WHERE status IN ('queued', 'retrying'); CREATE INDEX idx_queue_status ON notifications.notification_queue(status); CREATE INDEX idx_queue_notification ON notifications.notification_queue(notification_id); CREATE INDEX idx_queue_retry ON notifications.notification_queue(next_retry_at) WHERE status = 'retrying' AND next_retry_at IS NOT NULL; COMMENT ON TABLE notifications.notification_queue IS 'Cola de procesamiento asincrono de notificaciones'; ``` **6. notifications.notification_logs** (NUEVA) ```sql CREATE TABLE notifications.notification_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), notification_id UUID NOT NULL REFERENCES notifications.notifications(id) ON DELETE CASCADE, queue_id UUID REFERENCES notifications.notification_queue(id), -- Channel info channel notifications.channel NOT NULL, -- Delivery status status VARCHAR(30) NOT NULL, -- 'sent', 'delivered', 'opened', 'clicked', 'bounced', 'complained', 'failed' -- Provider info provider VARCHAR(50), -- 'sendgrid', 'ses', 'web-push' provider_message_id VARCHAR(200), provider_response JSONB, -- Timestamps delivered_at TIMESTAMPTZ, opened_at TIMESTAMPTZ, clicked_at TIMESTAMPTZ, -- Error details error_code VARCHAR(50), error_message TEXT, -- Audit created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL ); -- Indexes CREATE INDEX idx_logs_notification ON notifications.notification_logs(notification_id); CREATE INDEX idx_logs_status ON notifications.notification_logs(status, created_at DESC); CREATE INDEX idx_logs_channel ON notifications.notification_logs(channel, created_at DESC); COMMENT ON TABLE notifications.notification_logs IS 'Historial de entregas y eventos de notificaciones'; ``` ### 4.2 Funciones SQL ```sql -- ============================================== -- FUNCION: Enviar notificacion (encolar) -- ============================================== CREATE OR REPLACE FUNCTION notifications.send_notification( p_user_id UUID, p_tenant_id UUID, p_template_code VARCHAR, p_variables JSONB DEFAULT '{}', p_priority VARCHAR DEFAULT 'normal' ) RETURNS UUID AS $$ DECLARE v_notification_id UUID; v_template RECORD; v_preferences RECORD; v_channel VARCHAR; v_priority_value INT; v_subject TEXT; v_body TEXT; BEGIN -- Obtener template SELECT * INTO v_template FROM notifications.templates WHERE code = p_template_code AND is_active = TRUE; IF NOT FOUND THEN RAISE EXCEPTION 'Template % not found', p_template_code; END IF; -- Obtener preferencias del usuario SELECT * INTO v_preferences FROM notifications.user_preferences WHERE user_id = p_user_id AND tenant_id = p_tenant_id; -- Calcular priority value v_priority_value := CASE p_priority WHEN 'urgent' THEN 10 WHEN 'high' THEN 5 WHEN 'normal' THEN 0 WHEN 'low' THEN -5 ELSE 0 END; -- Renderizar template (basico - reemplazar {{variables}}) v_subject := v_template.subject; v_body := v_template.body; FOR key, value IN SELECT * FROM jsonb_each_text(p_variables) LOOP v_subject := REPLACE(v_subject, '{{' || key || '}}', value); v_body := REPLACE(v_body, '{{' || key || '}}', value); END LOOP; -- Crear notificacion base INSERT INTO notifications.notifications ( tenant_id, user_id, template_id, template_code, channel, subject, body, body_html, status, priority, metadata ) VALUES ( p_tenant_id, p_user_id, v_template.id, p_template_code, v_template.channel, v_subject, v_body, v_template.body_html, 'pending', p_priority::notifications.priority, p_variables ) RETURNING id INTO v_notification_id; -- Encolar para cada canal habilitado -- In-app siempre IF v_preferences IS NULL OR v_preferences.in_app_enabled THEN INSERT INTO notifications.notification_queue ( notification_id, channel, priority_value, status ) VALUES ( v_notification_id, 'in_app', v_priority_value, 'queued' ); END IF; -- Email si habilitado IF v_template.channel = 'email' AND (v_preferences IS NULL OR v_preferences.email_enabled) THEN INSERT INTO notifications.notification_queue ( notification_id, channel, priority_value, status ) VALUES ( v_notification_id, 'email', v_priority_value, 'queued' ); END IF; -- Push si habilitado y hay dispositivos IF (v_preferences IS NULL OR v_preferences.push_enabled) AND EXISTS ( SELECT 1 FROM notifications.user_devices WHERE user_id = p_user_id AND is_active = TRUE ) THEN INSERT INTO notifications.notification_queue ( notification_id, channel, priority_value, status ) VALUES ( v_notification_id, 'push', v_priority_value, 'queued' ); END IF; RETURN v_notification_id; END; $$ LANGUAGE plpgsql; -- ============================================== -- FUNCION: Obtener items pendientes de la cola -- ============================================== CREATE OR REPLACE FUNCTION notifications.get_pending_queue_items( p_limit INT DEFAULT 100 ) RETURNS TABLE ( id UUID, notification_id UUID, channel notifications.channel, priority_value INT, attempts INT, user_id UUID, tenant_id UUID, subject VARCHAR, body TEXT, body_html TEXT, recipient_email VARCHAR, device_tokens TEXT[] ) AS $$ BEGIN RETURN QUERY WITH pending AS ( SELECT q.* FROM notifications.notification_queue q WHERE q.status IN ('queued', 'retrying') AND (q.scheduled_for IS NULL OR q.scheduled_for <= NOW()) AND (q.next_retry_at IS NULL OR q.next_retry_at <= NOW()) ORDER BY q.priority_value DESC, q.created_at ASC LIMIT p_limit FOR UPDATE SKIP LOCKED ) SELECT p.id, p.notification_id, p.channel, p.priority_value, p.attempts, n.user_id, n.tenant_id, n.subject, n.body, n.body_html, n.recipient_email, ARRAY( SELECT d.device_token FROM notifications.user_devices d WHERE d.user_id = n.user_id AND d.is_active = TRUE ) AS device_tokens FROM pending p JOIN notifications.notifications n ON n.id = p.notification_id; END; $$ LANGUAGE plpgsql; -- ============================================== -- FUNCION: Marcar item de cola como procesado -- ============================================== CREATE OR REPLACE FUNCTION notifications.complete_queue_item( p_queue_id UUID, p_status VARCHAR, p_error_message TEXT DEFAULT NULL ) RETURNS VOID AS $$ DECLARE v_attempts INT; v_max_attempts INT; BEGIN -- Obtener intentos actuales SELECT attempts, max_attempts INTO v_attempts, v_max_attempts FROM notifications.notification_queue WHERE id = p_queue_id; IF p_status = 'sent' THEN UPDATE notifications.notification_queue SET status = 'sent', completed_at = NOW(), last_attempt_at = NOW(), attempts = v_attempts + 1 WHERE id = p_queue_id; ELSIF p_status = 'failed' THEN IF v_attempts + 1 >= v_max_attempts THEN -- Fallo definitivo UPDATE notifications.notification_queue SET status = 'failed', last_attempt_at = NOW(), attempts = v_attempts + 1, error_message = p_error_message, error_count = error_count + 1 WHERE id = p_queue_id; ELSE -- Programar retry con backoff exponencial UPDATE notifications.notification_queue SET status = 'retrying', last_attempt_at = NOW(), attempts = v_attempts + 1, next_retry_at = NOW() + (POWER(2, v_attempts) * INTERVAL '1 minute'), error_message = p_error_message, error_count = error_count + 1 WHERE id = p_queue_id; END IF; END IF; END; $$ LANGUAGE plpgsql; ``` --- ## 5. Backend - Servicios ### 5.1 Estructura de Archivos ``` apps/backend/src/modules/notifications/ ├── notifications.module.ts ├── controllers/ │ ├── notifications.controller.ts # CRUD notificaciones │ ├── preferences.controller.ts # Preferencias usuario │ └── devices.controller.ts # Registro dispositivos ├── services/ │ ├── notifications.service.ts # Orquestacion │ ├── notification-queue.service.ts # Procesamiento cola │ ├── push-notification.service.ts # Web Push API │ └── template.service.ts # Renderizado templates ├── gateways/ │ └── notifications.gateway.ts # WebSocket real-time ├── processors/ │ └── notification.processor.ts # BullMQ worker ├── dto/ │ ├── create-notification.dto.ts │ ├── send-template.dto.ts │ ├── update-preferences.dto.ts │ └── register-device.dto.ts ├── entities/ │ ├── notification.entity.ts │ ├── notification-template.entity.ts │ ├── user-preference.entity.ts │ ├── user-device.entity.ts │ ├── notification-queue.entity.ts │ └── notification-log.entity.ts └── __tests__/ ├── notifications.service.spec.ts ├── push-notification.service.spec.ts └── notification.processor.spec.ts ``` ### 5.2 PushNotificationService ```typescript // Implementacion usando web-push (sin Firebase) import * as webpush from 'web-push'; @Injectable() export class PushNotificationService implements OnModuleInit { private readonly logger = new Logger(PushNotificationService.name); onModuleInit() { // Configurar VAPID webpush.setVapidDetails( process.env.VAPID_SUBJECT || 'mailto:admin@example.com', process.env.VAPID_PUBLIC_KEY, process.env.VAPID_PRIVATE_KEY, ); } async sendToUser(userId: string, tenantId: string, payload: PushPayload): Promise { const devices = await this.deviceRepository.find({ where: { user_id: userId, tenant_id: tenantId, is_active: true }, }); const results: SendResult[] = []; for (const device of devices) { try { const subscription = JSON.parse(device.device_token); await webpush.sendNotification(subscription, JSON.stringify(payload)); results.push({ deviceId: device.id, success: true }); // Actualizar last_used_at device.last_used_at = new Date(); await this.deviceRepository.save(device); } catch (error) { if (error.statusCode === 410) { // Subscription expirada, marcar como inactiva device.is_active = false; await this.deviceRepository.save(device); this.logger.warn(`Device ${device.id} subscription expired, marked inactive`); } results.push({ deviceId: device.id, success: false, error: error.message }); } } return results; } getVapidPublicKey(): string { return process.env.VAPID_PUBLIC_KEY; } } ``` ### 5.3 NotificationsGateway (WebSocket) ```typescript @WebSocketGateway({ namespace: '/notifications', cors: { origin: '*' }, }) export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; private userSockets = new Map>(); // userId -> socketIds handleConnection(client: Socket) { const userId = this.extractUserId(client); if (userId) { if (!this.userSockets.has(userId)) { this.userSockets.set(userId, new Set()); } this.userSockets.get(userId).add(client.id); } } handleDisconnect(client: Socket) { const userId = this.extractUserId(client); if (userId && this.userSockets.has(userId)) { this.userSockets.get(userId).delete(client.id); } } // Llamado por NotificationService cuando se crea notificacion in-app async emitToUser(userId: string, notification: Notification) { const socketIds = this.userSockets.get(userId); if (socketIds) { for (const socketId of socketIds) { this.server.to(socketId).emit('notification:created', notification); } } } @SubscribeMessage('notification:read') async handleMarkAsRead(client: Socket, notificationId: string) { // Emitir a otros dispositivos del usuario const userId = this.extractUserId(client); this.server.to([...this.userSockets.get(userId)]).emit('notification:read', notificationId); } } ``` ### 5.4 NotificationProcessor (BullMQ) ```typescript @Processor('notifications') export class NotificationProcessor { constructor( private readonly emailService: EmailService, private readonly pushService: PushNotificationService, private readonly gateway: NotificationsGateway, private readonly queueService: NotificationQueueService, ) {} @Process('send') async handleSend(job: Job) { const { queueItemId, channel, notification } = job.data; try { switch (channel) { case 'email': await this.processEmail(notification); break; case 'push': await this.processPush(notification); break; case 'in_app': await this.processInApp(notification); break; } await this.queueService.completeItem(queueItemId, 'sent'); } catch (error) { await this.queueService.completeItem(queueItemId, 'failed', error.message); throw error; // BullMQ maneja retry } } private async processEmail(notification: NotificationData) { await this.emailService.sendEmail({ to: { email: notification.recipientEmail }, subject: notification.subject, html: notification.bodyHtml || notification.body, text: notification.body, }); } private async processPush(notification: NotificationData) { await this.pushService.sendToUser( notification.userId, notification.tenantId, { title: notification.subject, body: notification.body, data: notification.metadata, }, ); } private async processInApp(notification: NotificationData) { await this.gateway.emitToUser(notification.userId, notification); } } ``` --- ## 6. Endpoints API ### 6.1 Notificaciones | Metodo | Endpoint | Descripcion | Autorizacion | |--------|----------|-------------|--------------| | GET | /notifications | Listar mis notificaciones | Usuario | | GET | /notifications/unread-count | Contador no leidas | Usuario | | PATCH | /notifications/:id/read | Marcar como leida | Usuario | | POST | /notifications/read-all | Marcar todas como leidas | Usuario | | DELETE | /notifications/:id | Eliminar | Usuario | | POST | /notifications/send | Enviar notificacion manual | Admin | | POST | /notifications/send-template | Enviar desde template | Admin | ### 6.2 Preferencias | Metodo | Endpoint | Descripcion | Autorizacion | |--------|----------|-------------|--------------| | GET | /notifications/preferences | Obtener preferencias | Usuario | | PATCH | /notifications/preferences | Actualizar preferencias | Usuario | ### 6.3 Dispositivos (Push) | Metodo | Endpoint | Descripcion | Autorizacion | |--------|----------|-------------|--------------| | GET | /notifications/devices | Mis dispositivos | Usuario | | POST | /notifications/devices | Registrar dispositivo | Usuario | | DELETE | /notifications/devices/:id | Eliminar dispositivo | Usuario | | GET | /notifications/devices/vapid-key | Clave VAPID publica | Publico | ### 6.4 Templates (Admin) | Metodo | Endpoint | Descripcion | Autorizacion | |--------|----------|-------------|--------------| | GET | /notifications/templates | Listar templates | Admin | | GET | /notifications/templates/:code | Obtener template | Admin | | POST | /notifications/templates | Crear template | SuperAdmin | | PATCH | /notifications/templates/:id | Actualizar template | SuperAdmin | --- ## 7. Frontend ### 7.1 Componentes Requeridos | Componente | Estado | Proposito | |------------|--------|-----------| | NotificationDrawer | Existente | Panel lateral de notificaciones | | NotificationItem | Existente | Item individual | | NotificationSettings | Existente | Configurar preferencias | | PushPermissionBanner | NUEVO | Solicitar permiso push | | DevicesManager | NUEVO | Gestionar dispositivos registrados | ### 7.2 Hooks Nuevos ```typescript // Push notifications usePushPermission(): { permission: string; requestPermission: () => Promise } useRegisterDevice(): UseMutationResult useUnregisterDevice(): UseMutationResult useDevices(): UseQueryResult useVapidPublicKey(): UseQueryResult // WebSocket useNotificationSocket(): { isConnected: boolean; notifications: Notification[]; onNewNotification: (callback) => void; } ``` ### 7.3 Service Worker ```javascript // public/sw.js - Para recibir push notifications self.addEventListener('push', (event) => { const data = event.data?.json() || {}; event.waitUntil( self.registration.showNotification(data.title || 'Nueva notificacion', { body: data.body, icon: '/icon-192.png', badge: '/badge-72.png', data: data.data, actions: data.actions, }) ); }); self.addEventListener('notificationclick', (event) => { event.notification.close(); if (event.action === 'view' && event.notification.data?.url) { event.waitUntil(clients.openWindow(event.notification.data.url)); } }); ``` --- ## 8. Configuracion ### 8.1 Variables de Entorno ```bash # Email (existente) EMAIL_PROVIDER=sendgrid # 'sendgrid' | 'ses' | 'smtp' SENDGRID_API_KEY=SG.xxx EMAIL_FROM=noreply@example.com # Push Notifications (NUEVO) VAPID_PUBLIC_KEY=BN4GvZtEZiZuqFJi... VAPID_PRIVATE_KEY=aB3cDefGh4IjKlM5... VAPID_SUBJECT=mailto:admin@example.com # Queue (Redis - existente para webhooks) REDIS_HOST=localhost REDIS_PORT=6379 # WebSocket WS_PORT=3001 # Si separado del HTTP ``` ### 8.2 Generar Claves VAPID ```bash # Instalar web-push globalmente o como dev dependency npm install -g web-push # Generar par de claves web-push generate-vapid-keys # Output: # Public Key: BN4GvZtEZiZuqFJiLNpT... # Private Key: aB3cDefGh4IjKlM5nOpQr6StUvWxYz... ``` --- ## 9. Plan de Implementacion ### 9.1 Fase 1: DDL y Entidades (2-3 horas) 1. Agregar enums nuevos a 02-enums.sql 2. Crear tabla user_devices 3. Crear tabla notification_queue 4. Crear tabla notification_logs 5. Crear funciones SQL 6. Actualizar seeds si necesario 7. Validar con recreacion de BD ### 9.2 Fase 2: Backend Core (4-5 horas) 1. Crear entidades TypeORM nuevas 2. Implementar PushNotificationService 3. Implementar NotificationQueueService 4. Implementar NotificationsGateway (WebSocket) 5. Crear NotificationProcessor (BullMQ) 6. Actualizar NotificationsService 7. Agregar endpoints de devices ### 9.3 Fase 3: Frontend (3-4 horas) 1. Agregar service worker para push 2. Crear hooks de push 3. Implementar PushPermissionBanner 4. Implementar DevicesManager 5. Conectar WebSocket para real-time 6. Actualizar NotificationSettings ### 9.4 Fase 4: Testing y Documentacion (2-3 horas) 1. Tests unitarios servicios nuevos 2. Tests integracion endpoints 3. Actualizar documentacion SAAS-007 4. Actualizar PROJECT-STATUS.md 5. Actualizar _MAP.md del proyecto --- ## 10. Dependencias NPM ### Backend ```json { "web-push": "^3.6.7", "@nestjs/websockets": "^10.x", "@nestjs/platform-socket.io": "^10.x", "socket.io": "^4.7.x" } ``` ### Frontend ```json { "socket.io-client": "^4.7.x" } ``` --- ## 11. Compatibilidad ### 11.1 Navegadores Web Push | Navegador | Version Minima | Notas | |-----------|----------------|-------| | Chrome | 50+ | Soporte completo | | Firefox | 44+ | Soporte completo | | Edge | 17+ | Soporte completo | | Safari | 16.4+ | Requiere iOS 16.4+ | | Opera | 37+ | Soporte completo | ### 11.2 Node.js - Minimo: Node.js 18 LTS - Recomendado: Node.js 20 LTS --- ## 12. Metricas de Exito | Metrica | Objetivo | |---------|----------| | Tasa de entrega email | > 98% | | Tasa de entrega push | > 95% | | Latencia in-app | < 100ms | | Cobertura tests | > 80% | | Tiempo procesamiento cola | < 5 seg promedio | --- ## 13. Referencias - [Novu - Open-source notifications](https://novu.co/) - [NotifMe SDK](https://github.com/notifme/notifme-sdk) - [Web Push Protocol](https://datatracker.ietf.org/doc/html/rfc8030) - [VAPID for Web Push](https://datatracker.ietf.org/doc/html/rfc8292) - [gamilit EXT-003 Implementation](../../../gamilit/docs/03-fase-extensiones/EXT-003-notificaciones/) --- **Ultima actualizacion:** 2026-01-07 **Autor:** Claude Code **Revision:** Pendiente aprobacion