
Chat y comunicados corporativos en tiempo real
Mensajería corporativa en tiempo real con chats grupales, encuestas, comunicados masivos y notificaciones push, integrada al ERP de la empresa.
Documentación técnica
Resumen
Plataforma de mensajería corporativa en tiempo real concebida como un módulo del ERP de Grupo Garza Limón. No es un chat genérico: vive dentro del ecosistema de la empresa, reutiliza la identidad del ERP y se embebe en sus páginas existentes. Cubre conversaciones 1:1 y grupales, mensajes con archivos (imagen, audio, video, documentos, PPTX), reacciones con emoji, respuestas/citas, recibos de lectura e indicadores de "escribiendo", además de encuestas tipo WhatsApp, comunicados masivos y notificaciones push a móvil.
Requerimientos
El sistema debía cubrir mensajería directa y grupal con soporte de media, reacciones, recibos de lectura y presencia en tiempo real, más encuestas estilo WhatsApp y comunicados masivos con confirmación de lectura por destinatario. Todo ello con baja latencia, resiliencia ante desconexiones y entrega garantizada de push vía cola persistente. Las restricciones del proyecto fueron igualmente relevantes: despliegue on-premise sobre la infraestructura existente sin contenedores, y un frontend que debía poder inyectarse en páginas legacy del ERP sin requerir un paso de build.
Arquitectura
Backend modular en NestJS organizado por dominio: auth, chat, comunicados, presence, gateway (WebSocket), aws y push. El ciclo de petición HTTP es: request → AuthGuard (verifica el JWT del header) → controlador → servicio → repositorio TypeORM, con un decorador @User() que inyecta el payload del JWT en cualquier punto. Para mantener los controladores limpios se crearon decoradores compuestos (GetEndpoint, PostEndpoint, PatchEndpoint, DeleteEndpoint) que combinan ruta, documentación Swagger y autenticación en una sola anotación.
La capa de tiempo real usa Socket.IO con un esquema de rooms: cada usuario entra a user:<idErp> y a una room chat:<chatId> por cada conversación a la que pertenece. Cuando cambian los participantes de un chat, el GatewayService mueve los sockets entre rooms. Los eventos salientes (mensaje:nuevo, mensaje:leido, chat:escribiendo, usuario:estatus, encuesta:voto, comunicado:reaccion) y entrantes (chat:typing, presence:status, chat:marcar_leido, chat:reaccionar) conviven sobre la misma conexión autenticada por JWT en el handshake.
La autorización fina se resuelve con un guard de permisos que lee los códigos de permiso del ERP embebidos en el JWT (565 eliminar chats, 566 eliminar mensajes, 567 crear comunicados, 568 eliminar comunicados). El backend no administra usuarios ni contraseñas: actúa como un resource server que confía en el ERP como emisor (SSO de hecho), lo que simplificó enormemente el modelo de seguridad.
Base de datos
El modelo de datos usa migraciones versionadas con TypeORM sobre PostgreSQL. Una decisión clave fue darle a los usuarios una doble identidad: el ERP identifica a cada empleado con un idErp (string) almacenado como clave externa única, mientras que internamente se usa un PK numérico autoincremental para los joins de TypeORM. Los usuarios se crean de forma perezosa: el registro nace en la primera conexión con JWT válido, sin necesidad de sincronización previa con el ERP. Al dar de baja a un empleado, un soft-delete combinado con un hook @AfterLoad enmascara su nombre, correo e imagen, manteniendo el historial de conversaciones coherente sin exponer datos del exempleado. Todas las entidades relevantes usan soft-delete.
Decisiones técnicas y trade-offs
La decisión de diseño más interesante fue modelar la encuesta como un mensaje (TipoMensaje.ENCUESTA) en lugar de como una entidad paralela. Gracias a ello, las encuestas heredan sin código adicional la persistencia, el orden cronológico, la paginación por cursor, la entrega por socket (mensaje:nuevo), los recibos de lectura, las citas, el soft-delete y la sincronización delta. Los datos propios de la encuesta viven en tablas hijas y se serializan dentro del mensaje. La votación tiene semántica de set (el cliente manda la selección completa y el servidor reconcilia en una transacción), lo que cubre con la misma lógica el cambio de voto (única) y el toggle (múltiple).
En contraste, los comunicados NO son mensajes: su semántica de entrega es distinta (one-shot, con estatus de lectura por destinatario y sin pertenecer a un chat), así que se modelaron aparte. Esta asimetría (reutilizar la abstracción de mensaje cuando encaja y separarla cuando no) se nota también en el push: los mensajes y encuestas pasan por la cola persistente queue_mensajes (cuya clave única es dispositivo+mensaje), mientras que las reacciones y los comunicados se envían directo por el SDK de Expo, porque son efímeros/one-shot y chocarían con esa constraint.
El frontend tiene dos clientes que comparten una capa de estado (chat-store.js): la app de chat completa en JavaScript vanilla y un widget en React pensado para inyectarse en páginas legacy del ERP cargando React y Socket.IO desde CDN. Ambos bundles son IIFE sin paso de build —se editan y despliegan directamente sirviendo el archivo—, una decisión que prioriza la simplicidad de despliegue frente a las herramientas modernas de desarrollo, impuesta por la necesidad de convivir con un frontend heredado sin alterar su pipeline.
Desarrollo, retos y mejoras futuras
Los retos centrales fueron la resiliencia y la coherencia. Para sobrevivir a reconexiones se implementó una sincronización delta por cursor: el cliente envía el último id de mensaje conocido por chat y recibe solo lo nuevo, evitando recargar historiales completos. La entrega de push se resolvió con un cron cada 5 segundos que procesa la cola con bloqueo pesimista para evitar doble envío, un cron por minuto que verifica los recibos de Expo, y limpieza automática de tokens muertos (DeviceNotRegistered).
Infraestructura
Despliegue on-premise sobre la infraestructura existente de la empresa: Node.js gestionado por PM2 con autorestart y límite de memoria de 1 GB. Dado que el sistema se integra directamente al ecosistema del ERP (mismo servidor, red privada), no se requirió contenerización ni un pipeline de CI/CD formal; el flujo de despliegue es nest build → node dist/main, mantenido deliberadamente simple para encajar con los procesos internos del equipo. La media se almacena en AWS S3, las notificaciones push se entregan vía Expo, Swagger queda deshabilitado en producción y el CORS está restringido a los orígenes del ERP.