- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
30 KiB
30 KiB
| id | title | type | status | priority | module | version | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|
| ET-SAAS-007 | Especificacion Tecnica Notifications v2 | TechnicalSpec | Published | P0 | notifications | 2.0.0 | 2026-01-08 | 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, NotifMe SDK, y articulos especializados:
- Arquitectura desacoplada: Separar logica de negocio de envios
- Event-driven: Usar event emitters para triggers
- Queue-based: Nunca enviar en el request principal
- Channel-agnostic: Definir notificacion base, mapear a canales
- Fallback strategies: Multi-proveedor con failover
- Rate limiting: Prevenir spam y sobrecarga
- 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
-- 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)
-- 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)
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)
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)
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
-- ==============================================
-- 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
// 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<SendResult[]> {
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)
@WebSocketGateway({
namespace: '/notifications',
cors: { origin: '*' },
})
export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
private userSockets = new Map<string, Set<string>>(); // 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)
@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<NotificationJobData>) {
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
// Push notifications
usePushPermission(): { permission: string; requestPermission: () => Promise<void> }
useRegisterDevice(): UseMutationResult
useUnregisterDevice(): UseMutationResult
useDevices(): UseQueryResult<Device[]>
useVapidPublicKey(): UseQueryResult<string>
// WebSocket
useNotificationSocket(): {
isConnected: boolean;
notifications: Notification[];
onNewNotification: (callback) => void;
}
7.3 Service Worker
// 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
# 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
# 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)
- Agregar enums nuevos a 02-enums.sql
- Crear tabla user_devices
- Crear tabla notification_queue
- Crear tabla notification_logs
- Crear funciones SQL
- Actualizar seeds si necesario
- Validar con recreacion de BD
9.2 Fase 2: Backend Core (4-5 horas)
- Crear entidades TypeORM nuevas
- Implementar PushNotificationService
- Implementar NotificationQueueService
- Implementar NotificationsGateway (WebSocket)
- Crear NotificationProcessor (BullMQ)
- Actualizar NotificationsService
- Agregar endpoints de devices
9.3 Fase 3: Frontend (3-4 horas)
- Agregar service worker para push
- Crear hooks de push
- Implementar PushPermissionBanner
- Implementar DevicesManager
- Conectar WebSocket para real-time
- Actualizar NotificationSettings
9.4 Fase 4: Testing y Documentacion (2-3 horas)
- Tests unitarios servicios nuevos
- Tests integracion endpoints
- Actualizar documentacion SAAS-007
- Actualizar PROJECT-STATUS.md
- Actualizar _MAP.md del proyecto
10. Dependencias NPM
Backend
{
"web-push": "^3.6.7",
"@nestjs/websockets": "^10.x",
"@nestjs/platform-socket.io": "^10.x",
"socket.io": "^4.7.x"
}
Frontend
{
"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
- NotifMe SDK
- Web Push Protocol
- VAPID for Web Push
- gamilit EXT-003 Implementation
Ultima actualizacion: 2026-01-07 Autor: Claude Code Revision: Pendiente aprobacion