Volver a proyectos
Chat y comunicados corporativos en tiempo real

Chat y comunicados corporativos en tiempo real

En progresoBackend en tiempo real (API REST + WebSocket) con widget de chat embebibleAbr–May 2026Grupo Garza Limón

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

Funcionales: chat directo y grupal; envío de media; reacciones, respuestas y recibos de lectura; presencia en tiempo real (conectado, inactivo, desconectado, vacaciones); encuestas con voto único o múltiple; comunicados masivos (a todos o segmentados) con reacciones y confirmación de lectura por destinatario; notificaciones push a usuarios desconectados; alta/baja/reactivación de usuarios dictada por el ERP; sincronización tras reconexión.

No funcionales: baja latencia en la entrega (WebSocket), resiliencia ante desconexiones (sync delta por cursor), entrega garantizada de push (cola persistente con reintentos y verificación de recibos), y autenticación sin gestionar credenciales propias (el ERP emite el JWT). Restricciones: despliegue on-premise sobre la infraestructura existente de la empresa (PM2, sin contenedores), y el frontend debe poder inyectarse en páginas legacy del ERP sin 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

PostgreSQL con TypeORM y migraciones versionadas (22 migraciones; synchronize: false, migrationsRun: true, de modo que el esquema se actualiza solo al arrancar). Modelo principal: usuarios, chats, usuarios_chat (membresía + rol), mensajes, estatus_mensajes (recibos de lectura), reacciones_mensajes, las tres tablas de encuestas (encuestas, encuestas_opciones, encuestas_votos), las de comunicados (comunicados, comunicado_destinatarios, comunicado_estatus, reacciones_comunicados), dispositivos_usuario (tokens push de Expo) y queue_mensajes (cola de entrega de push).

Una decisión de modelado clave es la doble identidad del usuario: el ERP identifica a cada empleado con un idErp (string), que se guarda 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. Al dar de baja a un empleado se hace soft-delete y un hook @AfterLoad enmascara su nombre, correo e imagen ("Usuario dado de baja"), de modo que el historial de conversaciones permanece 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 (chat.bundle.js, ~7.700 líneas) en JavaScript vanilla y un widget en React (~6.400 líneas) 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 directamente. El trade-off es claro: se gana simplicidad de despliegue (basta servir el archivo) a cambio de perder herramientas modernas de desarrollo, una restricción impuesta por tener que convivir con un frontend heredado.

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

Hay un detalle conocido y documentado: el campo de estado de usuario viaja como "estado" por la API REST pero como "estatus" por los eventos de socket y el endpoint de presencia, una inconsistencia que obliga a tener cuidado al tocar cualquiera de las dos capas. Como mejoras futuras, el esquema ya reserva campos para encuestas anónimas, cerradas y con expiración; y a nivel de escalado, dado que hoy corre como una sola instancia de PM2, escalar horizontalmente el WebSocket requeriría añadir un adaptador (p. ej. Redis) para Socket.IO.

Infraestructura

Despliegue on-premise sin contenedores: Node.js gestionado por PM2 (instancia única, autorestart, reinicio por límite de 1 GB de memoria, puerto 3000 en producción). El código vive en un GitLab self-hosted (git.redgl.com); no hay pipeline de CI/CD ni Dockerfile (build y arranque manuales: nest build → node dist/main). La media se almacena en AWS S3 (subida con borrado del archivo previo en reemplazos). Swagger queda deshabilitado en producción. El CORS está restringido a los orígenes del ERP. Las notificaciones push se entregan vía Expo. El ERP es el integrador principal: emite los JWT y llama a endpoints internos para dar de baja, reactivar y forzar el estado de vacaciones de los usuarios.

Galería