Novu — Notificaciones persistentes en el OMS¶
Fecha: 2026-08-03 · Estado: ✅ MVP cerrado y funcionando end-to-end (2026-08-04)
Documentos de esta carpeta:
| Documento | Para qué |
|---|---|
| Este README | Análisis: el problema, qué es Novu, arquitectura del MVP, setup del ambiente y demo |
| guia-desarrollo.md | Mapa guía para desarrolladores: recetas para agregar notificaciones nuevas, convenciones, cómo probar, troubleshooting |
| roadmap-features.md | Backlog de features de notificación pendientes, por fases, mapeado sobre los eventos existentes del backend |
1. El problema¶
Hoy todas las notificaciones del OMS son efímeras:
- El backend emite por Socket.io (
NotificationsGateway) a salas (user-{salesPersonCode}, roles, salas de dominio). - El frontend las muestra como toasts de react-toastify (
WebSocketContext.tsx→notify()), que desaparecen a los segundos. - No hay persistencia en ningún lado: si el vendedor no está conectado en ese momento (logout, otro dispositivo, pestaña cerrada), la notificación se pierde para siempre. La campana del header era un mock con datos estáticos.
2. Qué es Novu y cómo encaja¶
Novu es una infraestructura de notificaciones open-source (self-hosted en nuestro caso). Conceptos clave:
| Concepto | Qué es | En nuestro caso |
|---|---|---|
| Subscriber | Un destinatario registrado en Novu | Cada usuario del OMS, identificado por su salesPersonCode |
| Workflow | Plantilla de notificación definida en el Dashboard (pasos: in-app, email, push…) | oms-in-app: un solo paso In-App que renderiza {{payload.title}} y {{payload.body}} |
| Trigger | Llamada del backend que dispara un workflow para un subscriber con un payload | NovuService.notifyUser(...) |
| Inbox | Componente React (campana + bandeja) que lee y marca notificaciones | <NotificationInbox /> en el header del dashboard |
Novu guarda cada notificación en su base de datos: el usuario la ve al volver a loguearse, desde cualquier dispositivo, con estado leído/no-leído. El socket de Novu (puerto 3002) solo agrega tiempo real cuando el usuario está conectado.
Importante: Novu complementa, no reemplaza al Socket.io actual. Los eventos de dominio (kanbans, comentarios, QR Bancard) siguen por el gateway; Novu se encarga de la bandeja persistente del usuario.
3. Arquitectura del MVP¶
flowchart LR
subgraph Backend NestJS
PS[payments.service] --> GW[NotificationsGateway.sendToUser]
GW -->|socket efímero| FE1[Toast react-toastify]
GW -->|"NovuService.notifyUser()"| NAPI
TC[POST /notifications/novu-test] --> NS[NovuService]
NS --> NAPI
end
subgraph Novu self-hosted 192.168.0.201
NAPI[API :3100] --> DB[(MongoDB - persistencia)]
DB --> WS[WebSocket :3002]
DASH[Dashboard :4000]
end
subgraph Frontend Next.js
WS -->|tiempo real| INBOX["Inbox (campana del header)"]
NAPI -->|historial al abrir| INBOX
end
Identidad unificada¶
El subscriberId de Novu es el salesPersonCode (como string) — el mismo identificador que ya usan las salas user-{salesPersonCode} del gateway. No hace falta sincronizar usuarios: Novu hace upsert del subscriber en cada trigger (el endpoint de prueba además le manda nombre y email).
4. Qué se implementó¶
Backend¶
| Archivo | Qué hace |
|---|---|
src/config/novu.config.ts |
Config registerAs('novu') + schema Joi (NOVU_API_URL, NOVU_SECRET_KEY, opcionales) |
src/services/novu/novu.module.ts |
Módulo siguiendo el patrón de cloudflare-images/algolia |
src/services/novu/novu.service.ts |
Cliente @novu/api apuntando al self-hosted. trigger() genérico + notifyUser() (workflow oms-in-app). Fire-and-forget: un fallo de Novu nunca rompe el flujo de negocio. Si faltan las env vars se deshabilita con un warning (el backend arranca igual) |
src/notifications/notifications.gateway.ts |
sendToUser() ahora también dispara Novu → cobros confirmados y anulados quedan persistidos sin tocar payments.service |
src/notifications/order-notifications.listener.ts |
Escucha order.created y notifica al vendedor del pedido (Pedido #, cliente, total, link a /orders/{docEntry}). El evento se enriqueció en orders.service.ts con docNum/cardName/total/salesPersonCode |
src/notifications/notifications.controller.ts |
POST /notifications/novu-test — dispara una notificación de prueba al usuario autenticado (título/cuerpo/url opcionales en el body) |
.env |
NOVU_API_URL=http://192.168.0.201:3100 + NOVU_SECRET_KEY= (completar) |
Frontend¶
| Archivo | Qué hace |
|---|---|
src/app/(DashboardLayout)/layout/vertical/header/NotificationInbox.tsx |
Ya existía (componente Inbox de @novu/nextjs con tema MUI adaptado a dark/light). Se corrigió el subscriberId: ahora usa salesPersonCode (antes user.id con un fallback hardcodeado que nunca iba a coincidir con lo que emite el backend) |
.env |
Se agregaron NEXT_PUBLIC_NOVU_BACKEND_URL y NEXT_PUBLIC_NOVU_SOCKET_URL (sin ellas el Inbox apuntaba a la nube de Novu, no a nuestro servidor) |
Dockerfile + build-and-push.sh + .env.build-staging.example |
Las 3 vars NEXT_PUBLIC_NOVU_* propagadas como build args (evita el problema conocido de "la var quedó fuera del build") |
5. Pasos para dejarlo funcionando (una sola vez)¶
- Secret key — Dashboard (
http://192.168.0.201:4000) → API Keys → copiar la Secret Key → pegarla enbackend/.envcomoNOVU_SECRET_KEY=.... ✅ Hecho (2026-08-03). Verificar de paso que el Application Identifier que muestra esa pantalla coincida con el delfrontend/.env(VOHKCgrgMUn2). - Workflow de Inbox — ✅ Creado vía API (2026-08-03) con un paso In-App que renderiza
{{payload.title}}/{{payload.body}}y redirige a{{payload.url}}. - ⚠️ Novu genera el identifier con un sufijo aleatorio (quedó
oms-in-app-11bs4j37), por eso el backend lo lee deNOVU_IN_APP_WORKFLOW_IDen el.env(defaultoms-in-app). Si el workflow se recrea en otro ambiente, actualizar esa variable con el identifier que muestre Dashboard → Workflows. - Levantar backend y frontend (
npm run start:dev/npm run dev). Recordar que ambos leen su.envsolo al arrancar.
6. Demo (el entregable)¶
Flujo principal — crear un pedido:
- Loguearse en el OMS. En el header aparece la campana del Inbox (reemplaza a la campana mock).
- Crear un pedido desde la app (
/orders/new) como se hace siempre. - Al confirmarse en SAP, el vendedor del pedido recibe en la campana: "Pedido #12345 creado para 'Cliente X' por GS 1.500.000" — en tiempo real si está conectado. Click en la notificación → navega al detalle del pedido.
- La prueba clave: cerrar sesión (o abrir otro navegador/dispositivo), volver a entrar → la notificación sigue ahí, con su estado de leída/no leída. Eso es lo que el sistema actual no puede hacer. Nota: si un Autorizador carga el pedido a nombre de un vendedor, la notificación le llega al vendedor, no al creador — se puede demostrar con dos usuarios.
Otros flujos que ya notifican: aprobar/anular un cobro (flujo de confirmación de pagos) → el vendedor recibe "Cobro confirmado"/"Cobro anulado" en su Inbox además del toast actual.
Prueba rápida sin flujo de negocio (requiere JWT): POST /notifications/novu-test desde Swagger (botón Authorize) o curl:
curl -X POST http://192.168.0.220:7000/api/notifications/novu-test \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"title": "Demo Novu", "body": "Notificación persistente 🎉"}'
7. Limitaciones del MVP y siguientes pasos¶
- Solo
sendToUserestá integrado (cobros confirmados/anulados). Los eventos por rol (new_payment_draft→ Autorizadores) necesitan Topics de Novu (suscripción de grupos) — siguiente iteración natural. - Sin HMAC: el Inbox se conecta con el
subscriberIden claro; cualquiera que conozca unsalesPersonCodepodría leer las notificaciones de otro. Para producción hay que habilitar HMAC encryption (backend firma elsubscriberIdcon la API key y el Inbox manda el hash). No es bloqueante en la red interna de pruebas. - URLs de producción/staging: cuando Novu pase de la máquina de pruebas (192.168.0.201) a un servidor definitivo, actualizar
NOVU_API_URL(backend) yNEXT_PUBLIC_NOVU_*(frontend, en el.env.build-*del ambiente). El Inbox del navegador debe poder alcanzar esas URLs directamente — en producción convendrá exponerlas vía Nginx con TLS (ej:novu-api.compulandia.com.py). - Alta de subscribers: hoy es lazy (upsert en el primer trigger). Si se quiere que todos los usuarios existan en Novu desde el día 1 (p. ej. para campañas), agregar el upsert en el login del SSO.
- Más canales: el mismo workflow puede sumar pasos de email/push/digest sin tocar el backend — solo se edita en el Dashboard. Candidato natural: reemplazar las tarjetas de Google Chat hardcodeadas del gateway.