Volver a proyectos
Portada del proyecto Chat y comunicados corporativos en tiempo real

Chat y comunicados corporativos en tiempo real

CompletadoBackend 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 ejecutivo

Plataforma de mensajería corporativa en tiempo real, construida como módulo nativo del ERP de Grupo Garza Limón (no un chat genérico aparte, sino integrado a la identidad visual y las páginas existentes del sistema).

Usada por toda la empresa: entre 300 y 500 usuarios concurrentes. Reemplazó el envío de documentos internos por canales no controlados (WhatsApp, correo, etc.), centralizando esos archivos en el chat corporativo con respaldo en AWS S3 — eliminando el riesgo de pérdida de documentos internos.

Cubre el ciclo completo de comunicación interna de una empresa: conversaciones 1:1 y grupales con archivos multimedia, encuestas tipo WhatsApp, y comunicados masivos con confirmación de lectura por destinatario — todo con entrega en tiempo real, resiliencia ante desconexiones, y notificaciones push garantizadas incluso con la app cerrada.

Mi rol

Fullstack, dentro de un equipo de 3: yo + un desarrollador mobile + un diseñador UI/UX.

Responsabilidad directa sobre:

  • Backend completo (NestJS, WebSockets, base de datos, integración con el ERP vía JWT, cola de push, S3).
  • Frontend web (app de chat en JS vanilla).
  • Widget embebible en React para inyectar en páginas legacy del ERP.

Además, fungí como líder técnico para la colaboración con el desarrollador mobile (consumo de la API y de los eventos de socket desde la app) y con el diseñador UI/UX (traducción de mockups a la implementación real, tanto en el chat web como en el widget).

Decisiones de arquitectura backend, modelo de datos, y los trade-offs de la sección 6 fueron mías. El diseño visual y de UX fue responsabilidad del diseñador del equipo.

Funcionalidades clave

  • Chat 1:1 y grupal en tiempo real
  • Envío de archivos: imágenes, audio, video, documentos y presentaciones (PPTX)
  • Reacciones con emoji a cualquier mensaje
  • Respuestas citando un mensaje específico
  • Recibos de lectura ("visto") e indicador de "escribiendo…"
  • Encuestas estilo WhatsApp, con voto único o múltiple
  • Comunicados masivos con confirmación de lectura por destinatario
  • Notificaciones push a dispositivos móviles, con reintentos automáticos
  • Widget embebible directamente en páginas ya existentes del ERP, sin modificar su código

Retos y cómo los resolví

Reconexiones e historiales pesados

  • Situación: los usuarios se desconectan constantemente (red corporativa, cambio de red, cierre de app), y recargar todo el historial en cada reconexión es lento e innecesario.
  • Acción: implementé sincronización delta por cursor — el cliente manda el último id de mensaje conocido por chat y el servidor solo devuelve lo nuevo.
  • Resultado: reconexión rápida sin recargar historiales completos, con estado consistente entre cliente y servidor.
Entrega de push sin duplicados ni tokens muertos
  • Situación: la entrega de notificaciones push necesitaba ser confiable, sin enviar la misma notificación dos veces ni acumular tokens de dispositivos desinstalados.
  • Acción: diseñé una cola persistente (queue_mensajes) con clave única dispositivo+mensaje, procesada por un cron cada 5 segundos con bloqueo pesimista para evitar doble envío; agregué un cron adicional que verifica recibos de Expo cada minuto y limpia tokens marcados como DeviceNotRegistered.
  • Resultado: entrega garantizada de push sin duplicados y sin acumulación de tokens inválidos.
Bajas de empleados sin romper el historial de conversaciones
  • Situación: cuando un empleado sale de la empresa, su información no debe seguir expuesta, pero las conversaciones donde participó deben seguir siendo legibles para el resto.
  • Acción: implementé soft-delete combinado con un hook @AfterLoad que enmascara nombre, correo e imagen del usuario dado de baja al momento de leer los datos.
  • Resultado: historial de chats íntegro y coherente, sin exponer datos de exempleados.
Despliegue on-premise sin contenedores, con frontend legacy sin build step
  • Situación: el proyecto debía correr sobre infraestructura existente sin Docker, y el frontend tenía que poder inyectarse en páginas legacy del ERP sin agregar un paso de build al pipeline heredado.
  • Acción: estructuré el backend para desplegarse con nest build && node dist/main bajo PM2, y construí los bundles de frontend (vanilla JS y widget en React) como IIFE que se editan y sirven directamente, sin compilación.
  • Resultado: integración sin fricción con los procesos de despliegue internos del equipo, a costa de renunciar a tooling moderno de frontend (trade-off consciente, ver sección 6).

Arquitectura

Backend modular en NestJS, organizado por dominio: auth, chat, comunicados, presence, gateway (WebSocket), aws y push.

Flujo de petición HTTP: request → AuthGuard (valida el JWT del header) → controlador → servicio → repositorio TypeORM, con un decorador @User() que inyecta el payload del JWT donde se necesite. 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.

Tiempo real: Socket.IO con 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, GatewayService mueve los sockets entre rooms. Eventos salientes: mensaje:nuevo, mensaje:leido, chat:escribiendo, usuario:estatus, encuesta:voto, comunicado:reaccion. Eventos entrantes: chat:typing, presence:status, chat:marcar_leido, chat:reaccionar. Todo sobre la misma conexión autenticada por JWT en el handshake.

Autorización: un guard de permisos lee 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 resource server que confía en el ERP como emisor (SSO de hecho), lo que simplificó el modelo de seguridad.

Base de datos: migraciones versionadas con TypeORM sobre PostgreSQL. Los usuarios tienen doble identidad: el ERP los identifica con un idErp (string) como clave externa única, mientras internamente se usa un PK numérico autoincremental para los joins. Los usuarios se crean de forma perezosa — el registro nace en la primera conexión con JWT válido, sin sincronización previa con el ERP. Todas las entidades relevantes usan soft-delete.

Decisiones técnicas y trade-offs

Encuesta modelada como mensaje, no como entidad paralela. Al tratar la encuesta como TipoMensaje.ENCUESTA, hereda sin código adicional la persistencia, el orden cronológico, la paginación por cursor, la entrega por socket, 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 tanto el voto único como el múltiple.

Comunicados modelados aparte, deliberadamente. Su semántica de entrega es distinta (one-shot, con estatus de lectura por destinatario, sin pertenecer a un chat), así que no se forzaron dentro de la abstracción de mensaje. Esta misma asimetría se refleja en el push: mensajes y encuestas pasan por la cola persistente queue_mensajes (clave única dispositivo+mensaje), mientras que reacciones y comunicados se envían directo por el SDK de Expo, porque son efímeros/one-shot y chocarían con esa constraint.

Frontend sin build step, por restricción del proyecto, no por preferencia. Dos clientes (app de chat completa en JS vanilla, y widget en React inyectable vía CDN) comparten una capa de estado (chat-store.js). Ambos se despliegan como bundles IIFE editables directamente. Esto prioriza la simplicidad de integración con un frontend legacy sobre las herramientas modernas de desarrollo — trade-off consciente y forzado por el contexto, no ideal en abstracto.

Infraestructura

Despliegue on-premise sobre infraestructura existente de la empresa: Node.js gestionado por PM2 con autorestart y límite de memoria de 1 GB. Al integrarse 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 simple a propósito para encajar con los procesos internos del equipo.

Media almacenada en AWS S3. Notificaciones push entregadas vía Expo. Swagger deshabilitado en producción. CORS restringido a los orígenes del ERP.

Galería

Chat corporativo en tiempo real con NestJS, WebSocket y PostgreSQL