template-saas/docs/02-especificaciones/ET-SAAS-007-notifications-v2.md
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:08 -06:00

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:

  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

-- 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)

  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

{
  "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


Ultima actualizacion: 2026-01-07 Autor: Claude Code Revision: Pendiente aprobacion