πŸš€ PLAN MAESTRO: Crear un Sistema Completo Tipo Slack πŸ“‹ ÍNDICE GENERAL DEL PROYECTO Parte Contenido Parte 1 βœ… AnΓ‘lisis completo de Slack: QuΓ© es, caracterΓ­sticas, funcionalidades, arquitectura tΓ©cnica Parte 2 πŸ—οΈ Arquitectura del sistema a construir + Stack tecnolΓ³gico recomendado Parte 3 πŸ—„οΈ DiseΓ±o de base de datos y modelo de datos completo Parte 4 πŸ’» Backend: APIs, servicios, WebSocket, real-time messaging Parte 5 🎨 Frontend: Web, Desktop, Mobile + UX/UI Parte 6 πŸ”§ Integraciones, AutomatizaciΓ³n, IA, Seguridad, Admin Parte 7 πŸ“¦ DevOps, Deployment, Escalabilidad, MonetizaciΓ³n y Roadmap πŸ“Œ PARTE 1: ANÁLISIS COMPLETO DE SLACK 1.1 ΒΏQUΓ‰ ES SLACK? Slack es una plataforma de comunicaciΓ³n y colaboraciΓ³n empresarial basada en la nube que reemplaza el email con un sistema de mensajerΓ­a en tiempo real organizado por canales. No es solo un chat: es un sistema operativo del trabajo que integra personas, herramientas, archivos, automatizaciΓ³n e IA en un solo lugar. MisiΓ³n de Slack "Hacer que la vida laboral de las personas sea mΓ‘s simple, mΓ‘s agradable y mΓ‘s productiva." Datos clave: 700M+ mensajes enviados diariamente 10M+ usuarios activos diarios 4M+ conexiones WebSocket concurrentes en pico 2.3M queries por segundo en la base de datos (pico) 30-40 deploys al dΓ­a al monolito principal 2,600+ integraciones disponibles Propiedad de Salesforce desde 2021 1.2 CARACTERÍSTICAS PRINCIPALES (TODAS LAS QUE DEBES REPLICAR) πŸ”Ή A. COMUNICACIΓ“N Y COLABORACIΓ“N Feature DescripciΓ³n Detalle tΓ©cnico Canales pΓΊblicos Espacios de conversaciΓ³n abiertos,任何人 puede unirse Organizados por equipo, proyecto o tema Canales privados Solo accesibles por invitaciΓ³n No aparecen en el buscador de canales Mensajes directos (DMs) Conversaciones 1-a-1 privadas Hasta 9 participantes en grupo Group DMs nombrados DMs grupales con nombre personalizado Nuevo feature 2025 Threads (Hilos) Respuestas dentro de un mensaje Mantiene conversaciones organizadas Mentions (@) Notificar a usuarios especΓ­ficos @usuario, @canal, @aquΓ­, @todos Reactions (Reacciones emoji) Reaccionar a mensajes con emojis Custom emojis disponibles Pinning mensajes Fijar mensajes importantes en el canal Visible en el panel del canal Bookmarks Marcar canales y enlaces importantes A nivel canal y personal Saved items Guardar mensajes para despuΓ©s Personal, no visible para otros Scheduled messages Programar mensajes para envΓ­o futuro Por fecha y hora Reminders Recordatorios personales o de canal Integrado con Slackbot Formatting rico Negrita, cursiva, cΓ³digo, listas, citas Markdown-like Code snippets Bloques de cΓ³digo con syntax highlighting Lenguajes detectados automΓ‘ticamente Link unfurling Vista previa de enlaces Procesamiento asΓ­ncrono Custom emojis Emojis personalizados por workspace Subidos por admins/users πŸ”Ή B. LLAMADAS Y VIDEO Feature DescripciΓ³n Huddles Audio/video llamadas instantΓ‘neas dentro de Slack Screen sharing Compartir pantalla en huddles Video llamadas grupales Hasta 50 participantes (plan Pro) Audio clips Grabar y enviar clips de audio Video clips Grabar y enviar clips de video Huddle notes (AI) Notas automΓ‘ticas de reuniones con IA Whiteboard Pizarra colaborativa en huddles TranscripciΓ³n TranscripciΓ³n automΓ‘tica de huddles πŸ”Ή C. GESTIΓ“N DE PROYECTOS Feature DescripciΓ³n Lists Listas de tareas estilo Trello/Asana dentro de Slack Canvases Documentos colaborativos ricos (texto, tablas, imΓ‘genes, video) Templates Plantillas para proyectos, onboarding, standups, etc. Tasks assignment Asignar tareas a personas con fechas Kanban board Vista tipo board para lists Comments en tasks Comentarios dentro de cada tarea Custom views Vistas filtradas y ordenadas de lists Alerts en lists Notificaciones cuando cambia un campo πŸ”Ή D. SLACK CONNECT (COLABORACIΓ“N EXTERNA) Feature DescripciΓ³n Shared channels Canales compartidos entre organizaciones DMs externos Mensajes directos con personas externas MΓΊltiples organizaciones Conectar con varios partners externos Control admin AprobaciΓ³n de canales compartidos por admins πŸ”Ή E. IA Y INTELIGENCIA (SLACK AI) Feature DescripciΓ³n Slackbot (AI Agent) Agente IA personal que conoce tu equipo y contexto Conversation summaries ResΓΊmenes automΓ‘ticos de canales y threads Huddle notes Notas automΓ‘ticas de reuniones con transcript Daily recaps Resumen diario de actividad en canales File summaries ResΓΊmenes de documentos compartidos AI Search BΓΊsqueda con respuestas generadas por IA Enterprise search BΓΊsqueda across todas las apps integradas Translations TraducciΓ³n de mensajes con IA Workflow generation Generar workflows con un prompt MCP (Model Context Protocol) Conectar herramientas externas al Slackbot Claude integration Chat con Claude directamente GitHub Copilot RevisiΓ³n de cΓ³digo dentro de Slack Agentforce AnΓ‘lisis de datos CRM dentro de Slack πŸ”Ή F. AUTOMATIZACIΓ“N Y WORKFLOWS Feature DescripciΓ³n Workflow Builder Crear automatizaciones sin cΓ³digo (drag & drop) Custom workflow steps Pasos personalizados con cΓ³digo Conditional branching LΓ³gica if/else en workflows Triggers EmisiΓ³n por emoji, join a canal, schedule, eventos externos Slack shortcuts Acciones rΓ‘pidas via teclado o menΓΊ Slash commands Comandos personalizados /comando Incoming webhooks Recibir datos de servicios externos Outgoing webhooks Enviar datos a servicios externos Bots Bots automatizados para tareas πŸ”Ή G. INTEGRACIONES Y PLATAFORMA Feature DescripciΓ³n App Directory 2,600+ apps integrables API REST API completa para desarrolladores RTM API (WebSocket) API de mensajerΓ­a en tiempo real Events API Webhooks para eventos Interactive components Botones, menΓΊs, modales interactivos Block Kit Framework para UI rica en mensajes Modals Ventanas emergentes interactivas App home PΓ‘gina de inicio personalizada para cada app OAuth 2.0 AutenticaciΓ³n para apps Custom apps Apps privadas para tu workspace πŸ”Ή H. BÚSQUEDA Y CONOCIMIENTO Feature DescripciΓ³n BΓΊsqueda global Buscar en mensajes, archivos, canales y personas Filtros de bΓΊsqueda Por tipo, fecha, persona, canal BΓΊsqueda con IA Preguntas en lenguaje natural con respuestas Enterprise search Buscar en apps externas conectadas Historial completo Todos los mensajes archivados y buscables πŸ”Ή I. NOTIFICACIONES Y PRESENCIA Feature DescripciΓ³n Presence (online/offline/away) Indicador de estado en tiempo real Custom status Estado personalizado con emoji y texto Do Not Disturb Silenciar notificaciones por horario Notification preferences ConfiguraciΓ³n granular por canal y tipo Keyword notifications Notificar cuando se menciona una palabra clave Push notifications Notificaciones mΓ³viles Email notifications ResΓΊmenes por email Mobile push Notificaciones push nativas iOS/Android πŸ”Ή J. ARCHIVOS Y COMPARTIR Feature DescripciΓ³n File sharing Subir y compartir archivos en canales y DMs File preview Vista previa de imΓ‘genes, PDFs, documentos Google Drive integration Compartir archivos de Google Drive Drag & drop Arrastrar archivos al chat File comments Comentarios en archivos Snippets Bloques de cΓ³digo compartibles πŸ”Ή K. ADMINISTRACIΓ“N Y GOBIERNO Feature DescripciΓ³n Workspace management GestiΓ³n del workspace completo Roles y permisos Owner, Admin, User, Guest, Multi-channel guest User groups Grupos personalizados (@equipo-marketing) SCIM provisioning Provisioning automΓ‘tico de usuarios via SSO Analytics MΓ©tricas de uso y actividad Audit logs Registro de acciones administrativas Data exports Exportar mensajes y datos Retention policies PolΓ­ticas de retenciΓ³n de mensajes y archivos Channel management Archivar, renombrar, convertir canales Custom roles Roles personalizados (Enterprise) Content flagging Marcar contenido inapropiado Domain claiming Reclamar dominios de email πŸ”Ή L. SEGURIDAD Y CUMPLIMIENTO Feature DescripciΓ³n Data encryption Cifrado en trΓ‘nsito y en reposo 2FA AutenticaciΓ³n de dos factores SSO (SAML) Single Sign-On con proveedores externos Session management DuraciΓ³n de sesiΓ³n configurable Device management GestiΓ³n de dispositivos nativos Enterprise Key Management Claves de cifrado propias (Enterprise) EMM Enterprise Mobility Management Information barriers Barreras de informaciΓ³n entre grupos Legal holds RetenciΓ³n legal de datos DLP PrevenciΓ³n de pΓ©rdida de datos HIPAA compliance Cumplimiento HIPAA (Enterprise) GDPR compliance Cumplimiento GDPR Data residency Datos almacenados en regiones especΓ­ficas Anomaly detection DetecciΓ³n de eventos anΓ³malos πŸ”Ή M. PLATFORMS Y CLIENTES Cliente DescripciΓ³n Web app Acceso via navegador Desktop Windows App nativa Windows Desktop macOS App nativa macOS Desktop Linux App nativa Linux iOS app App nativa iPhone/iPad Android app App nativa Android Apple Watch App para Apple Watch 1.3 ARQUITECTURA TΓ‰CNICA DE SLACK (CΓ“MO FUNCIONA POR DENTRO) Slack tiene una arquitectura fascinante que ha evolucionado en 10+ aΓ±os. AquΓ­ estΓ‘ el resumen tΓ©cnico: Stack TecnolΓ³gico de Slack: Componente TecnologΓ­a Backend principal (Webapp) PHP/Hack corriendo en HHVM (monolito de ~1M lΓ­neas) Base de datos MySQL con Vitess (sharding middleware) Cache Memcached con Mcrouter MensajerΓ­a en tiempo real Java (Channel Servers, Gateway Servers, Admin Servers, Presence Servers) Load balancer Envoy (migrado desde HAProxy) Edge cache Flannel (cache personalizado a nivel aplicaciΓ³n) Service discovery Consul Event streaming Kafka β†’ S3 (Parquet via Secor) Analytics Presto, Hive, Spark en AWS EMR BΓΊsqueda Apache Solr con ML re-ranking (SVM) Job queue Redis (9 billΓ³n jobs/dΓ­a) Control plane Rotor (xDS personalizado para Envoy) Infraestructura AWS (us-east-1 principal, multi-regiΓ³n para data residency) Servicios de Tiempo Real (Real-Time Messaging): Slack tiene 4 tipos de servidores para mensajerΓ­a en tiempo real: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CLIENT (Browser/App) β”‚ β”‚ WebSocket Connection β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ENVOY LOAD BALANCER β”‚ β”‚ (TLS termination, routing) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ GATEWAY SERVER (GS) β”‚ β”‚ β€’ Stateful, in-memory β”‚ β”‚ β€’ Termina WebSocket connections β”‚ β”‚ β€’ Mantiene suscripciones de canales por usuario β”‚ β”‚ β€’ Desplegado en mΓΊltiples regiones geogrΓ‘ficas β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CHANNEL SERVER (CS) β”‚ β”‚ β€’ Stateful, in-memory β”‚ β”‚ β€’ ~16M channels por host β”‚ β”‚ β€’ Consistent hashing para mapear canales a servidores β”‚ β”‚ β€’ Pub/sub: recibe mensaje β†’ broadcast a todos los GS β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ADMIN SERVER (AS) β”‚ β”‚ β€’ Stateless β”‚ β”‚ β€’ Interfaz entre Webapp y Channel Servers β”‚ β”‚ β€’ Routea mensajes al CS correcto via hash ring β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ PRESENCE SERVER (PS) β”‚ β”‚ β€’ In-memory β”‚ β”‚ β€’ Trackea quΓ© usuarios estΓ‘n online β”‚ β”‚ β€’ Notifica cambios de presencia β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Flujo de un mensaje: Usuario A escribe mensaje β†’ HTTP POST al Webapp Webapp persiste el mensaje en MySQL/Vitess Webapp envΓ­a el evento al Admin Server (AS) AS hashea el channel_id β†’ encuentra el Channel Server (CS) responsable CS recibe el mensaje β†’ lo broadcastea a todos los Gateway Servers (GS) globales que tienen suscriptores en ese canal Cada GS empuja el mensaje via WebSocket a los clientes conectados Latencia objetivo: < 500ms global end-to-end Arquitectura Celular (Disponibilidad 99.99%): Slack usa una arquitectura celular donde cada AZ (Availability Zone) opera como una cΓ©lula independiente: Servicios dentro de un AZ solo hablan con servicios del mismo AZ Si un AZ falla, el trΓ‘fico se redirige en 5 minutos con granularidad de 1% Usan Envoy + Rotor (xDS custom) para peso dinΓ‘mico por AZ 1.4 PLANES Y PRECIOS DE SLACK Plan Precio Features clave Free $0 90 dΓ­as de historial, 10 integraciones, 1:1 huddles Pro $7.25-8.75/usuario/mes Historial ilimitado, huddles grupales, workflows, AI bΓ‘sico Business+ $12.50-15/usuario/mes SSO SAML, data exports, compliance, AI avanzado Enterprise+ Custom EKM, HIPAA, DLP, information barriers, audit logs Slack AI +$10/usuario/mes Summaries, recaps, AI search, Slackbot 1.5 CΓ“MO SLACK AYUDA A UNA EMPRESA Problemas que resuelve: Problema SoluciΓ³n Slack Email excesivo Reemplaza cadenas de email con canales organizados ComunicaciΓ³n dispersa Centraliza todo en un solo lugar Falta de contexto Historial completo buscable + IA summaries Silos de informaciΓ³n Canales pΓΊblicos transparentes Trabajo manual repetitivo Workflow Builder automatiza tareas ColaboraciΓ³n externa difΓ­cil Slack Connect con partners/clientes Reuniones innecesarias Huddles rΓ‘pidos async Onboarding lento Historial de canales + templates + canvases BΓΊsqueda de informaciΓ³n Enterprise search + AI-powered search Falta de visibilidad Canales pΓΊblicos, lists, analytics Beneficios medibles: 90% de usuarios dicen que mejora la comunicaciΓ³n 87% dicen que colaboran mΓ‘s eficientemente 86% sienten que mejorΓ³ el trabajo remoto 88% se sienten mΓ‘s conectados con su equipo 97 minutos ahorrados por semana con IA 35% mΓ‘s tiempo ahorrado por automatizaciones βœ… RESUMEN DE LA PARTE 1 Hemos cubierto: βœ… QuΓ© es Slack y su propΓ³sito βœ… Todas las funcionalidades categorizadas (13 categorΓ­as, 100+ features) βœ… Arquitectura tΓ©cnica completa (stack, servidores, flujo de mensajes) βœ… Planes y precios βœ… CΓ³mo ayuda a una empresa πŸ“Œ PARTE 2: ARQUITECTURA DEL SISTEMA + STACK TECNOLΓ“GICO 2.1 FILOSOFÍA DE DISEΓ‘O Antes de elegir tecnologΓ­as, debemos definir los principios arquitectΓ³nicos que guiarΓ‘n todo el proyecto: Principio DescripciΓ³n Escalabilidad horizontal Cada componente debe poder escalar aΓ±adiendo mΓ‘s instancias Tiempo real primero La mensajerΓ­a en tiempo real es el core, todo gira alrededor de esto Multi-tenant Soportar mΓΊltiples workspaces/empresas en una sola instalaciΓ³n Modular Cada servicio es independiente pero se comunican vΓ­a APIs bien definidas Seguridad por diseΓ±o Cifrado, RBAC, auditorΓ­a desde el dΓ­a 1 API-first Todo funciona a travΓ©s de APIs (web, mobile, integraciones) Resilencia Tolerancia a fallos, circuit breakers, retries, dead letter queues Observabilidad Logs, mΓ©tricas, traces en cada servicio desde el inicio Developer experience FΓ‘cil de desarrollar, testear y desplegar 2.2 STACK TECNOLΓ“GICO RECOMENDADO Vamos a comparar lo que usa Slack vs lo que te recomiendo (tecnologΓ­as mΓ‘s modernas y accesibles): πŸ”Ή A. BACKEND PRINCIPAL (API + LΓ“GICA DE NEGOCIO) Componente Slack usa Recomendado Por quΓ© Lenguaje principal PHP/Hack (HHVM) Node.js + TypeScript Ecoistema real-time, npm, WebSocket nativo, misma lengua que frontend Alternativa - Go (Golang) Alta concurrencia, compilado, rΓ‘pido, ideal para microservicios Framework Custom NestJS (Node) o Gin/Fiber (Go) Estructura modular, DI, decoradores, scalable Monolito vs Microservicios Monolito PHP + servicios Modular Monolith β†’ Microservicios gradual Empezar simple, extraer servicios cuando sea necesario πŸ”Ή B. MENSAJERÍA EN TIEMPO REAL Componente Slack usa Recomendado Por quΓ© WebSocket Server Java custom (Gateway Servers) Socket.io o ws (Node) / gorilla/websocket (Go) Maduro, battle-tested, simple Pub/Sub interno Java custom (Channel Servers) Redis Pub/Sub o NATS Pub/sub ultra rΓ‘pido, simple de operar Message queue Redis (Job Queue) BullMQ (Node) / Asynq (Go) sobre Redis Queue con retries, delays, priorities Event bus Kafka Redis Streams (inicio) β†’ Kafka (escala) Empezar simple con Redis, migrar cuando sea necesario πŸ”Ή C. BASE DE DATOS Componente Slack usa Recomendado Por quΓ© DB principal (relacional) MySQL + Vitess PostgreSQL MΓ‘s features, JSON nativo, mejor extensibilidad, full-text search integrado Cache Memcached + Mcrouter Redis (cache + pub/sub + queues) Un solo componente para mΓΊltiples usos BΓΊsqueda Apache Solr + ML re-ranking Elasticsearch o Meilisearch (inicio) Elasticsearch mΓ‘s potente, Meilisearch mΓ‘s simple para empezar Almacenamiento de archivos S3 AWS S3 o MinIO (self-hosted) S3 estΓ‘ndar de industria, MinIO compatible S3 self-hosted Time-series / Analytics Presto/Hive/Spark ClickHouse o TimescaleDB ClickHouse ultra rΓ‘pido para analytics πŸ”Ή D. FRONTEND Componente Slack usa Recomendado Por quΓ© Web app React (custom) React 18+ con Next.js SSR, routing, SEO, ecoistema enorme Estado global Custom Zustand o Redux Toolkit Zustand mΓ‘s simple, Redux mΓ‘s escalable WebSocket cliente Custom Socket.io-client Auto-reconnect, rooms, fallback a polling UI Components Custom (Slack Kit) shadcn/ui + Tailwind CSS Componentes hermosos, customizables, accesibles Editor de texto rico Custom TipTap (basado en ProseMirror) Editor rico extensible, usado por Notion, GitLab Tabla/Kanban (Lists) Custom TanStack Table + custom board Tablas potentes, sorting, filtering, virtualization πŸ”Ή E. DESKTOP Y MOBILE Componente Slack usa Recomendado Por quΓ© Desktop (Win/Mac/Linux) Electron custom Tauri o Electron Tauri: mΓ‘s ligero (Rust + webview), Electron: mΓ‘s maduro Mobile (iOS/Android) Nativo Swift/Kotlin React Native o Flutter Un solo cΓ³digo para ambas plataformas, mΓ‘s rΓ‘pido de desarrollar Notificaciones push Custom Firebase Cloud Messaging (FCM) + APNs EstΓ‘ndar de industria, gratuito πŸ”Ή F. INFRAESTRUCTURA Y DEVOPS Componente Slack usa Recomendado Por quΓ© Cloud AWS AWS o Google Cloud o DigitalOcean (mΓ‘s econΓ³mico) AWS mΓ‘s completo, DO mΓ‘s simple y barato para empezar Contenedores Custom Docker + Kubernetes EstΓ‘ndar de industria OrquestaciΓ³n inicial - Docker Compose (dev) β†’ Kubernetes (prod) Empezar simple, escalar cuando sea necesario CI/CD Custom (ReleaseBot) GitHub Actions o GitLab CI Integrado con Git, gratuito para empezar Load balancer Envoy Nginx (inicio) β†’ Envoy (escala) Nginx mΓ‘s simple para empezar Reverse proxy Envoy Nginx o Caddy Caddy: HTTPS automΓ‘tico, config simple Service discovery Consul Kubernetes DNS o Consul K8s DNS suficiente al inicio Monitoreo Custom + Prometheus Prometheus + Grafana EstΓ‘ndar open-source Logs Custom Loki o ELK Stack Loki mΓ‘s ligero, ELK mΓ‘s potente Tracing Custom Jaeger o OpenTelemetry Distributed tracing estΓ‘ndar CDN Custom (Flannel) Cloudflare o AWS CloudFront CDN global, cache en edge πŸ”Ή G. IA Y INTELIGENCIA Componente Slack usa Recomendado Por quΓ© LLM (Chatbot) Claude + propios OpenAI GPT-4 o Anthropic Claude o Llama 3 (self-hosted) APIs potentes, fΓ‘cil integraciΓ³n Vector DB (RAG) Custom pgvector (PostgreSQL) o Pinecone o Qdrant pgvector: integrado en PG, simple Embeddings Custom OpenAI Embeddings o Cohere Calidad y velocidad TranscripciΓ³n audio Custom Whisper (OpenAI) Open-source, self-hostable Translation Custom DeepL API o Google Translate Calidad de traducciΓ³n πŸ”Ή H. SEGURIDAD Y AUTENTICACIΓ“N Componente Slack usa Recomendado Por quΓ© AutenticaciΓ³n Custom + SAML Auth0, Clerk, o Keycloak (self-hosted) Auth0/Clerk: managed, Keycloak: open-source completo SSO SAML custom SAML + OAuth2 + OIDC via Keycloak/Auth0 EstΓ‘ndares de industria Cifrado AES-256 AES-256 (en reposo) + TLS 1.3 (en trΓ‘nsito) EstΓ‘ndar Secrets management Custom HashiCorp Vault o AWS Secrets Manager GestiΓ³n segura de secretos 2.3 ARQUITECTURA DEL SISTEMA COMPLETO πŸ”Ή DIAGRAMA DE ARQUITECTURA GENERAL β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CLIENTES β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Web β”‚ β”‚ Desktop β”‚ β”‚ Mobile β”‚ β”‚ β”‚ β”‚(Next.js)β”‚ β”‚(Tauri) β”‚ β”‚(RN/Flutr)β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CDN (Cloudflare) β”‚ β”‚ Assets estΓ‘ticos, cache edge β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ LOAD BALANCER (Nginx/Envoy) β”‚ β”‚ TLS termination, routing, rate limiting β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ HTTP/REST β”‚ β”‚ WebSocket β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ API GATEWAY β”‚ β”‚ REAL-TIME GATEWAY β”‚ β”‚ (NestJS/Go) β”‚ β”‚ (WebSocket Server) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β€’ Auth middleware β”‚ β”‚ β€’ Connection manager β”‚ β”‚ β€’ Rate limiting β”‚ β”‚ β€’ Room subscription β”‚ β”‚ β€’ Request routing β”‚ β”‚ β€’ Event broadcasting β”‚ β”‚ β€’ API versioning β”‚ β”‚ β€’ Presence tracking β”‚ β”‚ β€’ Swagger/OpenAPI β”‚ β”‚ β€’ Typing indicators β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ SERVICIOS BACKEND β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Message β”‚ β”‚ Channel β”‚ β”‚ User β”‚ β”‚ File β”‚ β”‚ β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Search β”‚ β”‚ Workflow β”‚ β”‚ AI β”‚ β”‚ Auth β”‚ β”‚ β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Notif. β”‚ β”‚ Integr. β”‚ β”‚ Analyticsβ”‚ β”‚ β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ DATA LAYER β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚PostgreSQLβ”‚ β”‚ Redis β”‚ β”‚Elastic- β”‚ β”‚ S3/ β”‚ β”‚ β”‚ β”‚ (Vitess- β”‚ β”‚ (Cache + β”‚ β”‚search β”‚ β”‚ MinIO β”‚ β”‚ β”‚ β”‚ style β”‚ β”‚ Pub/Sub β”‚ β”‚ (Search) β”‚ β”‚ (Files) β”‚ β”‚ β”‚ β”‚ sharding)β”‚ β”‚ + Queue) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚pgvector β”‚ β”‚ ClickHouseβ”‚ β”‚ β”‚ β”‚(Vector DBβ”‚ β”‚ (Analyticsβ”‚ β”‚ β”‚ β”‚ for AI) β”‚ β”‚ Metrics) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ 2.4 DESGLOSE DE CADA SERVICIO πŸ”Ή 1. API Gateway Responsabilidad: Punto de entrada ΓΊnico para todas las peticiones HTTP/REST Endpoints principales: β”œβ”€β”€ /api/v1/auth/* β†’ Login, register, logout, refresh token β”œβ”€β”€ /api/v1/users/* β†’ CRUD usuarios, perfil, status, presence β”œβ”€β”€ /api/v1/workspaces/* β†’ CRUD workspaces, settings β”œβ”€β”€ /api/v1/channels/* β†’ CRUD canales, miembros, archivar β”œβ”€β”€ /api/v1/messages/* β†’ Enviar, editar, borrar, threads, reacciones β”œβ”€β”€ /api/v1/files/* β†’ Upload, download, preview, delete β”œβ”€β”€ /api/v1/search/* β†’ BΓΊsqueda global con IA β”œβ”€β”€ /api/v1/dms/* β†’ Mensajes directos y grupos β”œβ”€β”€ /api/v1/huddles/* β†’ Llamadas audio/video β”œβ”€β”€ /api/v1/lists/* β†’ Lists, tasks, kanban β”œβ”€β”€ /api/v1/canvases/* β†’ Documentos colaborativos β”œβ”€β”€ /api/v1/workflows/* β†’ Automatizaciones β”œβ”€β”€ /api/v1/integrations/* β†’ Apps externas, webhooks, bots β”œβ”€β”€ /api/v1/admin/* β†’ AdministraciΓ³n, analytics, audit β”œβ”€β”€ /api/v1/notifications/* β†’ Preferencias, push tokens └── /api/v1/ai/* β†’ Slackbot, summaries, search IA CaracterΓ­sticas: β”œβ”€β”€ JWT authentication β”œβ”€β”€ Rate limiting (por usuario, por IP, por workspace) β”œβ”€β”€ Request validation (Zod / Joi) β”œβ”€β”€ API versioning β”œβ”€β”€ CORS configuration β”œβ”€β”€ Swagger/OpenAPI docs auto-generadas β”œβ”€β”€ Request ID tracking (para tracing) └── Circuit breakers a servicios internos πŸ”Ή 2. Real-Time Gateway (WebSocket Server) Responsabilidad: Mantener conexiones WebSocket persistentes con clientes Funciones: β”œβ”€β”€ ConexiΓ³n WebSocket por usuario β”œβ”€β”€ AutenticaciΓ³n via token al conectar β”œβ”€β”€ SuscripciΓ³n a canales (rooms) β”œβ”€β”€ DesuscripciΓ³n de canales β”œβ”€β”€ Broadcast de mensajes en tiempo real β”œβ”€β”€ Typing indicators β”œβ”€β”€ Presence (online/offline/away) β”œβ”€β”€ Reacciones en tiempo real β”œβ”€β”€ Notificaciones de canal β”œβ”€β”€ Actualizaciones de perfil β”œβ”€β”€ Eventos de sistema (usuario uniΓ³/saliΓ³ de canal) └── Heartbeat/ping-pong para mantener conexiΓ³n Eventos que maneja (similares a Slack RTM): β”œβ”€β”€ message β†’ Nuevo mensaje en canal β”œβ”€β”€ message_changed β†’ Mensaje editado β”œβ”€β”€ message_deleted β†’ Mensaje borrado β”œβ”€β”€ reaction_added β†’ ReacciΓ³n agregada β”œβ”€β”€ reaction_removed β†’ ReacciΓ³n quitada β”œβ”€β”€ channel_join β†’ Usuario se uniΓ³ a canal β”œβ”€β”€ channel_leave β†’ Usuario saliΓ³ de canal β”œβ”€β”€ channel_created β†’ Nuevo canal creado β”œβ”€β”€ channel_archive β†’ Canal archivado β”œβ”€β”€ user_typing β†’ Usuario escribiendo β”œβ”€β”€ presence_change β†’ Cambio de presencia β”œβ”€β”€ user_status_change β†’ Cambio de status β”œβ”€β”€ file_shared β†’ Archivo compartido β”œβ”€β”€ pin_added β†’ Mensaje fijado β”œβ”€β”€ pin_removed β†’ Mensaje desfijado β”œβ”€β”€ huddle_start β†’ Huddle iniciado β”œβ”€β”€ huddle_end β†’ Huddle terminado β”œβ”€β”€ list_updated β†’ Lista/tarea actualizada β”œβ”€β”€ canvas_updated β†’ Canvas actualizado └── bot_message β†’ Mensaje de bot/app Escalabilidad: β”œβ”€β”€ Sticky sessions (usuario β†’ servidor fijo) β”œβ”€β”€ Redis Pub/Sub para cross-server broadcasting β”œβ”€β”€ Horizontal scaling (N instancias stateless) └── Connection draining graceful en deploy πŸ”Ή 3. Message Service Responsabilidad: LΓ³gica de negocio de mensajes Funciones: β”œβ”€β”€ Crear mensaje (texto, rico, cΓ³digo, archivo) β”œβ”€β”€ Editar mensaje (con tracking de ediciΓ³n) β”œβ”€β”€ Borrar mensaje (soft delete) β”œβ”€β”€ Threads/hilos (respuestas anidadas) β”œβ”€β”€ Reacciones emoji β”œβ”€β”€ Pin messages β”œβ”€β”€ Scheduled messages β”œβ”€β”€ Mentions parsing (@user, @channel, @here) β”œβ”€β”€ Link unfurling (preview de URLs) β”œβ”€β”€ Message formatting (markdown β†’ HTML) β”œβ”€β”€ Message search indexing (β†’ Elasticsearch) β”œβ”€β”€ Message retention policies └── Message export Modelo de mensaje: β”œβ”€β”€ id: UUID β”œβ”€β”€ workspace_id: UUID β”œβ”€β”€ channel_id: UUID β”œβ”€β”€ user_id: UUID β”œβ”€β”€ thread_ts: timestamp (si es reply) β”œβ”€β”€ text: texto plano β”œβ”€β”€ blocks: array (Block Kit format) β”œβ”€β”€ attachments: array β”œβ”€β”€ files: array de file_ids β”œβ”€β”€ reactions: array de {emoji, users[]} β”œβ”€β”€ mentions: array de user_ids β”œβ”€β”€ edited: {ts, user_id} β”œβ”€β”€ is_pinned: boolean β”œβ”€β”€ is_scheduled: boolean β”œβ”€β”€ scheduled_for: timestamp β”œβ”€β”€ created_at: timestamp └── updated_at: timestamp πŸ”Ή 4. Channel Service Responsabilidad: GestiΓ³n de canales y membresΓ­a Funciones: β”œβ”€β”€ Crear canal (pΓΊblico/privado) β”œβ”€β”€ Archivar/desarchivar canal β”œβ”€β”€ Renombrar canal β”œβ”€β”€ Set topic y purpose β”œβ”€β”€ Gestionar miembros (join/leave/invite/kick) β”œβ”€β”€ Migrar canal (pΓΊblico ↔ privado) β”œβ”€β”€ Shared channels (Slack Connect) β”œβ”€β”€ Channel recommendations β”œβ”€β”€ Default channels (obligatorios) β”œβ”€β”€ Channel posting permissions β”œβ”€β”€ Channel notification settings └── Channel bookmarks/pins Tipos de canal: β”œβ”€β”€ public_channel β†’ Abierto a todos en el workspace β”œβ”€β”€ private_channel β†’ Solo por invitaciΓ³n β”œβ”€β”€ group_dm β†’ Mensaje grupal privado β”œβ”€β”€ dm β†’ Mensaje directo 1:1 β”œβ”€β”€ shared_channel β†’ Compartido entre workspaces └── external_dm β†’ DM con usuario externo πŸ”Ή 5. User Service Responsabilidad: GestiΓ³n de usuarios, perfiles y presencia Funciones: β”œβ”€β”€ CRUD usuarios β”œβ”€β”€ Perfil (avatar, nombre, tΓ­tulo, bio, telΓ©fono, email) β”œβ”€β”€ Status custom (emoji + texto + expiraciΓ³n) β”œβ”€β”€ Presence (online/away/dnd/offline) β”œβ”€β”€ Timezone β”œβ”€β”€ Preferencias de notificaciΓ³n β”œβ”€β”€ User groups (grupos personalizados) β”œβ”€β”€ Roles y permisos (owner/admin/user/guest) β”œβ”€β”€ Deactivate/reactivate usuarios β”œβ”€β”€ Guest accounts (single-channel / multi-channel) └── Session management Presencia: β”œβ”€β”€ Heartbeat cada 30s desde cliente β”œβ”€β”€ Auto-away despuΓ©s de 5 min inactivo β”œβ”€β”€ Presence broadcast solo a usuarios relevantes β”œβ”€β”€ OptimizaciΓ³n: no notificar a todos NΒ², solo visibles └── Presencia persistida en Redis (TTL) πŸ”Ή 6. File Service Responsabilidad: Upload, almacenamiento y distribuciΓ³n de archivos Funciones: β”œβ”€β”€ Upload de archivos (drag & drop, paste) β”œβ”€β”€ Almacenamiento en S3/MinIO β”œβ”€β”€ Thumbnail generation (imΓ‘genes) β”œβ”€β”€ File preview (imΓ‘genes, PDFs, videos, code) β”œβ”€β”€ File sharing en canales y DMs β”œβ”€β”€ File comments β”œβ”€β”€ File deletion (con retention policy) β”œβ”€β”€ Virus scanning (ClamAV opcional) β”œβ”€β”€ File size limits configurables └── File type restrictions Almacenamiento: β”œβ”€β”€ S3 bucket por workspace (o prefijo) β”œβ”€β”€ URLs firmadas (presigned URLs) para download β”œβ”€β”€ CDN para assets pΓΊblicos β”œβ”€β”€ Multipart upload para archivos grandes └── Image processing (resize, optimize) con Sharp πŸ”Ή 7. Search Service Responsabilidad: BΓΊsqueda en todo el contenido del workspace Funciones: β”œβ”€β”€ Indexar mensajes en tiempo real β”œβ”€β”€ Indexar archivos (nombre, contenido) β”œβ”€β”€ Indexar canales (nombre, topic, purpose) β”œβ”€β”€ Indexar usuarios (nombre, email, tΓ­tulo) β”œβ”€β”€ BΓΊsqueda full-text β”œβ”€β”€ Filtros (tipo, fecha, autor, canal) β”œβ”€β”€ BΓΊsqueda con IA (preguntas en lenguaje natural) β”œβ”€β”€ Autocomplete / suggestions β”œβ”€β”€ BΓΊsqueda en integraciones externas (enterprise search) └── Re-ranking con ML (opcional, fase avanzada) TecnologΓ­a: β”œβ”€β”€ Elasticsearch (o Meilisearch para empezar) β”œβ”€β”€ Index por workspace β”œβ”€β”€ Sync via event stream (message.created β†’ index) β”œβ”€β”€ Fuzzy matching β”œβ”€β”€ Highlighting de resultados └── Typo tolerance πŸ”Ή 8. AI Service Responsabilidad: Todas las funcionalidades de IA Funciones: β”œβ”€β”€ Slackbot (chatbot con contexto del workspace) β”œβ”€β”€ Conversation summaries (canal, thread, DM) β”œβ”€β”€ Daily recaps (resumen de actividad) β”œβ”€β”€ Huddle notes (notas de reuniΓ³n con transcript) β”œβ”€β”€ File summaries (resumen de documentos) β”œβ”€β”€ AI Search (respuestas en lenguaje natural) β”œβ”€β”€ Translations β”œβ”€β”€ Workflow generation (crear workflow desde prompt) β”œβ”€β”€ Code review (integraciΓ³n con GitHub) └── Sentiment analysis (opcional) Arquitectura RAG (Retrieval Augmented Generation): β”œβ”€β”€ Embeddings de mensajes guardados en pgvector β”œβ”€β”€ Cuando usuario hace pregunta β†’ buscar contexto relevante β”œβ”€β”€ Construir prompt con contexto + pregunta β”œβ”€β”€ Enviar a LLM (GPT-4/Claude/Llama) β”œβ”€β”€ Respuesta con citations/fuentes └── Fallback: si no hay contexto, respuesta genΓ©rica Seguridad: β”œβ”€β”€ No compartir datos con proveedores LLM (usar API privada) β”œβ”€β”€ No usar datos de clientes para entrenar modelos β”œβ”€β”€ Puedes self-hostear Llama 3 para mΓ‘xima privacidad └── Rate limiting en queries IA πŸ”Ή 9. Workflow Service Responsabilidad: AutomatizaciΓ³n de tareas Funciones: β”œβ”€β”€ Workflow Builder visual (drag & drop, no-code) β”œβ”€β”€ Triggers: β”‚ β”œβ”€β”€ Emoji reaction β”‚ β”œβ”€β”€ User joins channel β”‚ β”œβ”€β”€ Scheduled (cron) β”‚ β”œβ”€β”€ Slash command β”‚ β”œβ”€β”€ Webhook externo β”‚ β”œβ”€β”€ Form submission β”‚ └── New message in channel β”œβ”€β”€ Steps: β”‚ β”œβ”€β”€ Send message β”‚ β”œβ”€β”€ Send DM β”‚ β”œβ”€β”€ Create channel β”‚ β”œβ”€β”€ Add user to channel β”‚ β”œβ”€β”€ Create list item β”‚ β”œβ”€β”€ HTTP request (webhook externo) β”‚ β”œβ”€β”€ Conditional (if/else branching) β”‚ β”œβ”€β”€ Variable assignment β”‚ β”œβ”€β”€ Wait/delay β”‚ └── Custom code step (JavaScript) β”œβ”€β”€ Workflow templates β”œβ”€β”€ Workflow analytics (ejecuciones, Γ©xito/fallo) └── Workflow logs EjecuciΓ³n: β”œβ”€β”€ Workflow engine (state machine) β”œβ”€β”€ Job queue (BullMQ/Asynq) β”œβ”€β”€ Timeout y retry policies β”œβ”€β”€ Variables contextuales (user, channel, message) └── Webhook signing (HMAC) πŸ”Ή 10. Integration Service Responsabilidad: Integraciones con apps externas Funciones: β”œβ”€β”€ App Directory (marketplace de integraciones) β”œβ”€β”€ OAuth 2.0 flow para apps externas β”œβ”€β”€ Incoming webhooks (recibir datos de fuera) β”œβ”€β”€ Outgoing webhooks (enviar datos a fuera) β”œβ”€β”€ Slash commands custom β”œβ”€β”€ Bots y bot users β”œβ”€β”€ Block Kit (UI components en mensajes) β”œβ”€β”€ Interactive components (botones, menus, modals) β”œβ”€β”€ App home (pΓ‘gina personalizada de la app) β”œβ”€β”€ Event subscriptions (webhook de eventos) β”œβ”€β”€ API tokens management └── Rate limiting por app Apps esenciales a integrar (fase 1): β”œβ”€β”€ Google Drive / Docs β”œβ”€β”€ Google Calendar β”œβ”€β”€ GitHub / GitLab β”œβ”€β”€ Jira β”œβ”€β”€ Zoom / Google Meet β”œβ”€β”€ Trello / Asana β”œβ”€β”€ Dropbox / OneDrive └── Zapier / n8n (conector universal) πŸ”Ή 11. Notification Service Responsabilidad: Entregar notificaciones a travΓ©s de mΓΊltiples canales Funciones: β”œβ”€β”€ Notificaciones push (FCM para Android, APNs para iOS) β”œβ”€β”€ Notificaciones desktop (electron/tauri native) β”œβ”€β”€ Notificaciones web (Web Push API) β”œβ”€β”€ Email notifications (digest, mentions, direct) β”œβ”€β”€ Notification preferences por usuario β”œβ”€β”€ Notification preferences por canal β”œβ”€β”€ Do Not Disturb (DND) schedules β”œβ”€β”€ Keyword notifications β”œβ”€β”€ Notification queue (rate limited) β”œβ”€β”€ Notification batching (agregar mΓΊltiples en una) β”œβ”€β”€ Read/unread tracking └── Badge count (unread counter) Reglas de notificaciΓ³n: β”œβ”€β”€ @mention directo β†’ notificaciΓ³n inmediata β”œβ”€β”€ @channel / @here β†’ notificaciΓ³n (configurable) β”œβ”€β”€ DM directo β†’ notificaciΓ³n inmediata β”œβ”€β”€ Mensaje en canal (no mention) β†’ configurable β”œβ”€β”€ Thread reply β†’ configurable β”œβ”€β”€ DND activo β†’ silenciar (excepto urgencias) β”œβ”€β”€ Mobile β†’ push notification β”œβ”€β”€ Desktop inactivo β†’ push notification └── Email β†’ digest cada X horas si hay no leΓ­dos πŸ”Ή 12. Huddle / Call Service Responsabilidad: Llamadas de audio y video Funciones: β”œβ”€β”€ Iniciar huddle desde canal o DM β”œβ”€β”€ Audio call (Opus codec) β”œβ”€β”€ Video call (VP8/VP9/H.264) β”œβ”€β”€ Screen sharing β”œβ”€β”€ Participants management (join/leave/mute) β”œβ”€β”€ Huddle chat (mensajes durante la llamada) β”œβ”€β”€ Huddle notes (IA transcripciΓ³n + resumen) β”œβ”€β”€ Recording (opcional) β”œβ”€β”€ Whiteboard colaborativo └── Push to talk (opcional) TecnologΓ­a: β”œβ”€β”€ WebRTC para audio/video P2P β”œβ”€β”€ SFU server (Selective Forwarding Unit) para grupos β”‚ β”œβ”€β”€ Mediasoup (recomendado, Node.js) β”‚ β”œβ”€β”€ Jitsi Videobridge (Java, open-source) β”‚ └── LiveKit (Go, moderno) β”œβ”€β”€ STUN/TURN servers (coturn) β”œβ”€β”€ Signaling via WebSocket (canal existente) └── GrabaciΓ³n β†’ S3 storage πŸ”Ή 13. Admin Service Responsabilidad: AdministraciΓ³n y gobierno del workspace Funciones: β”œβ”€β”€ Dashboard de analytics β”‚ β”œβ”€β”€ Mensajes enviados (dΓ­a/semana/mes) β”‚ β”œβ”€β”€ Usuarios activos (DAU/MAU) β”‚ β”œβ”€β”€ Canales activos β”‚ β”œβ”€β”€ Huddles realizados β”‚ β”œβ”€β”€ Apps mΓ‘s usadas β”‚ └── Workflows ejecutados β”œβ”€β”€ User management β”‚ β”œβ”€β”€ Invitar usuarios β”‚ β”œβ”€β”€ Deactivate/reactivate β”‚ β”œβ”€β”€ Cambiar roles β”‚ β”œβ”€β”€ Guest accounts β”‚ └── Bulk import/export β”œβ”€β”€ Channel management β”‚ β”œβ”€β”€ Archivar canales inactivos β”‚ β”œβ”€β”€ Channel posting permissions β”‚ β”œβ”€β”€ Default channels β”‚ └── Channel naming conventions β”œβ”€β”€ Security β”‚ β”œβ”€β”€ Session management β”‚ β”œβ”€β”€ 2FA enforcement β”‚ β”œβ”€β”€ SSO configuration β”‚ β”œβ”€β”€ API token management β”‚ └── Data retention policies β”œβ”€β”€ Compliance β”‚ β”œβ”€β”€ Audit logs β”‚ β”œβ”€β”€ Data exports β”‚ β”œβ”€β”€ Legal holds β”‚ β”œβ”€β”€ Information barriers β”‚ └── DLP policies └── Billing (si es SaaS) β”œβ”€β”€ Planes y features β”œβ”€β”€ Usage tracking β”œβ”€β”€ Invoice generation └── Payment gateway (Stripe) 2.5 DECISIONES DE DISEΓ‘O CLAVE πŸ”Ή DecisiΓ³n 1: Modular Monolith β†’ Microservicios FASE 1 (MVP): Monolito modular β”œβ”€β”€ Un solo deployable con mΓ³dulos internos β”œβ”€β”€ ComunicaciΓ³n via funciΓ³n calls (no network) β”œβ”€β”€ Shared database con schemas separados └── Ventaja: Simple de desarrollar y desplegar FASE 2 (Crecimiento): Extraer servicios crΓ­ticos β”œβ”€β”€ Separar Real-Time Gateway (necesita escalar independiente) β”œβ”€β”€ Separar AI Service (necesita GPU/recursos especiales) β”œβ”€β”€ Separar File Service (necesita S3 access) └── El resto permanece en monolito FASE 3 (Escala): Microservicios completos β”œβ”€β”€ Cada servicio independiente β”œβ”€β”€ ComunicaciΓ³n via gRPC o message bus β”œβ”€β”€ Database por servicio └── API Gateway para coordinaciΓ³n πŸ”Ή DecisiΓ³n 2: Sharding Strategy Como Slack, pero simplificado: NIVEL 1 (MVP): Single PostgreSQL β”œβ”€β”€ Una instancia, tablas con workspace_id β”œβ”€β”€ Índices en workspace_id + channel_id └── Suficiente para miles de usuarios NIVEL 2 (Crecimiento): Read replicas + partitioning β”œβ”€β”€ Partitioning por workspace_id ( declarative partitioning PG) β”œβ”€β”€ Read replicas para bΓΊsquedas β”œβ”€β”€ Redis cache para hot queries └── Suficiente para decenas de miles NIVEL 3 (Escala): Sharding horizontal β”œβ”€β”€ Sharding por channel_id (como Slack con Vitess) β”œβ”€β”€ Citus extension para PostgreSQL (sharding nativo) β”œβ”€β”€ O PgBouncer + sharding manual └── Suficiente para cientos de miles πŸ”Ή DecisiΓ³n 3: Real-Time Architecture NIVEL 1 (MVP): Single Socket.io server β”œβ”€β”€ Una instancia Socket.io β”œβ”€β”€ Redis adapter para pub/sub (multiple instancias) β”œβ”€β”€ Rooms = channels └── Suficiente para ~10,000 conexiones concurrentes NIVEL 2 (Crecimiento): Multiple Socket.io + Redis β”œβ”€β”€ N instancias Socket.io detrΓ‘s de load balancer β”œβ”€β”€ Sticky sessions (IP hash o cookie) β”œβ”€β”€ Redis Pub/Sub para cross-server broadcast β”œβ”€β”€ Connection counter en Redis └── Suficiente para ~100,000 conexiones NIVEL 3 (Escala): Custom WebSocket gateway β”œβ”€β”€ Gateway Servers (como Slack) β”œβ”€β”€ Channel Servers (consistent hashing) β”œβ”€β”€ Presence Servers separados β”œβ”€β”€ Admin Server para routing └── Suficiente para millones de conexiones πŸ”Ή DecisiΓ³n 4: Multi-tenancy OpciΓ³n A: Shared database, shared schema (recomendado inicio) β”œβ”€β”€ Todas las empresas comparten la misma BD β”œβ”€β”€ workspace_id en cada tabla β”œβ”€β”€ Row-level security (RLS) en PostgreSQL β”œβ”€β”€ MΓ‘s econΓ³mico, mΓ‘s simple └── MigraciΓ³n fΓ‘cil OpciΓ³n B: Shared database, separate schemas β”œβ”€β”€ Cada empresa tiene su schema en PostgreSQL β”œβ”€β”€ MΓ‘s aislamiento β”œβ”€β”€ MΓ‘s complejo de mantener └── Para empresas que lo requieran OpciΓ³n C: Separate databases (Enterprise) β”œβ”€β”€ Cada empresa tiene su propia BD β”œβ”€β”€ MΓ‘ximo aislamiento β”œβ”€β”€ Self-hosted o dedicated instances └── Para compliance extremo 2.6 DIAGRAMA DE DESPLIEGUE (DEV β†’ PROD) πŸ”Ή Entorno de Desarrollo docker-compose.yml β”œβ”€β”€ api β†’ NestJS (hot reload) β”œβ”€β”€ websocket β†’ Socket.io server (hot reload) β”œβ”€β”€ web β†’ Next.js dev server β”œβ”€β”€ postgres β†’ PostgreSQL 16 β”œβ”€β”€ redis β†’ Redis 7 β”œβ”€β”€ minio β†’ MinIO (S3 compatible) β”œβ”€β”€ meilisearch β†’ Meilisearch (bΓΊsqueda) β”œβ”€β”€ mailhog β†’ Email testing └── nginx β†’ Reverse proxy local Todo corre en localhost con un comando: $ docker-compose up -d πŸ”Ή Entorno de Staging β”œβ”€β”€ Kubernetes cluster (GKE/EKS/DOKS) β”œβ”€β”€ 1 pod API, 1 pod WebSocket, 1 pod Web β”œβ”€β”€ PostgreSQL managed (RDS/CloudSQL) β”œβ”€β”€ Redis managed (ElastiCache) β”œβ”€β”€ S3 bucket para archivos β”œβ”€β”€ Elasticsearch managed (o self-hosted) β”œβ”€β”€ Cloudflare CDN β”œβ”€β”€ GitHub Actions CI/CD └── Secrets en Vault/Secrets Manager πŸ”Ή Entorno de ProducciΓ³n β”œβ”€β”€ Kubernetes cluster multi-AZ β”œβ”€β”€ API: 3+ pods (auto-scaling 3-20) β”œβ”€β”€ WebSocket: 3+ pods (sticky sessions) β”œβ”€β”€ Web: 2+ pods (CDN fronted) β”œβ”€β”€ PostgreSQL: primary + 2 read replicas β”œβ”€β”€ Redis: cluster mode (3 primary + 3 replica) β”œβ”€β”€ Elasticsearch: 3 node cluster β”œβ”€β”€ S3 + CloudFront (CDN) β”œβ”€β”€ Cloudflare (WAF + DDoS protection) β”œβ”€β”€ Prometheus + Grafana (monitoreo) β”œβ”€β”€ Loki (logs centralizados) β”œβ”€β”€ Jaeger (tracing) β”œβ”€β”€ Vault (secrets) └── GitHub Actions β†’ ArgoCD (GitOps deploy) 2.7 RESUMEN DEL STACK FINAL RECOMENDADO β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ STACK FINAL β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ FRONTEND β”‚ β”‚ Web: React 18 + Next.js 14 + Tailwind + shadcn β”‚ β”‚ Desktop: Tauri (Rust + WebView) β”‚ β”‚ Mobile: React Native (Expo) β”‚ β”‚ State: Zustand + React Query (TanStack) β”‚ β”‚ Editor: TipTap (ProseMirror) β”‚ β”‚ WebSocket: Socket.io-client β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ BACKEND β”‚ β”‚ API: NestJS (TypeScript) + Prisma ORM β”‚ β”‚ WebSocket: Socket.io + Redis Adapter β”‚ β”‚ Queue: BullMQ (Redis-based) β”‚ β”‚ Real-time: Redis Pub/Sub β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ DATA β”‚ β”‚ DB: PostgreSQL 16 (con pgvector) β”‚ β”‚ Cache: Redis 7 β”‚ β”‚ Search: Meilisearch (inicio) β†’ Elasticsearch β”‚ β”‚ Files: MinIO (dev) β†’ S3 (prod) β”‚ β”‚ Analytics: ClickHouse β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ IA β”‚ β”‚ LLM: OpenAI GPT-4o o Claude 3.5 o Llama 3 β”‚ β”‚ Embeddings: OpenAI text-embedding-3-small β”‚ β”‚ Vector: pgvector (integrado en PostgreSQL) β”‚ β”‚ Audio: Whisper (transcripciΓ³n) β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ INFRA β”‚ β”‚ Cloud: DigitalOcean (inicio) β†’ AWS (escala) β”‚ β”‚ Container: Docker + Kubernetes β”‚ β”‚ CI/CD: GitHub Actions + ArgoCD β”‚ β”‚ CDN: Cloudflare β”‚ β”‚ Monitor: Prometheus + Grafana + Loki + Jaeger β”‚ β”‚ Auth: Clerk o Keycloak (self-hosted) β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ AUDIO/VIDEO β”‚ β”‚ WebRTC: Mediasoup (SFU) β”‚ β”‚ STUN/TURN: coturn β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ SEGURIDAD β”‚ β”‚ Auth: Clerk / Keycloak (SSO, SAML, OAuth2) β”‚ β”‚ Secrets: HashiCorp Vault β”‚ β”‚ WAF: Cloudflare β”‚ β”‚ Scan: ClamAV (file scanning) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ πŸ“Œ PARTE 3: DISEΓ‘O DE BASE DE DATOS Y MODELO DE DATOS COMPLETO 3.1 VISIΓ“N GENERAL DEL MODELO DE DATOS El sistema tendrΓ‘ 6 esquemas principales en PostgreSQL: postgrez_chat (database) β”œβ”€β”€ schema: auth β†’ Usuarios, sesiones, tokens, SSO β”œβ”€β”€ schema: workspace β†’ Workspaces, membresΓ­a, roles, invitaciones β”œβ”€β”€ schema: messaging β†’ Canales, mensajes, threads, reacciones β”œβ”€β”€ schema: files β†’ Archivos, thumbnails, compartidos β”œβ”€β”€ schema: automation β†’ Workflows, integraciones, bots, webhooks β”œβ”€β”€ schema: analytics β†’ MΓ©tricas, audit logs, usage tracking └── schema: ai β†’ Embeddings, vectores, contexto IA 3.2 ESQUEMA: AUTH (AutenticaciΓ³n y Usuarios) πŸ”Ή Tabla: users CREATE TABLE auth.users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, email_verified BOOLEAN DEFAULT FALSE, phone VARCHAR(30), phone_verified BOOLEAN DEFAULT FALSE, password_hash VARCHAR(255), -- NULL si usa SSO ΓΊnicamente display_name VARCHAR(80) NOT NULL, real_name VARCHAR(80), avatar_url VARCHAR(500), title VARCHAR(120), -- Job title bio TEXT, timezone VARCHAR(50) DEFAULT 'UTC', locale VARCHAR(10) DEFAULT 'en', pronouns VARCHAR(30), status_text VARCHAR(100), status_emoji VARCHAR(50), status_expires TIMESTAMPTZ, presence VARCHAR(15) DEFAULT 'offline', -- online, away, dnd, offline last_active_at TIMESTAMPTZ, is_bot BOOLEAN DEFAULT FALSE, is_deleted BOOLEAN DEFAULT FALSE, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT chk_presence CHECK (presence IN ('online','away','dnd','offline')) ); -- Índices CREATE INDEX idx_users_email ON auth.users(email); CREATE INDEX idx_users_presence ON auth.users(presence) WHERE is_deleted = FALSE; CREATE INDEX idx_users_last_active ON auth.users(last_active_at); πŸ”Ή Tabla: user_identities (SSO / OAuth connections) CREATE TABLE auth.user_identities ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, provider VARCHAR(50) NOT NULL, -- google, github, saml, okta, etc. provider_user_id VARCHAR(255) NOT NULL, -- ID en el proveedor provider_email VARCHAR(255), provider_data JSONB, -- Datos extra del proveedor created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(provider, provider_user_id) ); CREATE INDEX idx_user_identities_user ON auth.user_identities(user_id); CREATE INDEX idx_user_identities_provider ON auth.user_identities(provider, provider_user_id); πŸ”Ή Tabla: sessions CREATE TABLE auth.sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, token_hash VARCHAR(255) NOT NULL UNIQUE, -- Hashed refresh token device_type VARCHAR(20), -- web, desktop, ios, android device_name VARCHAR(120), device_id VARCHAR(255), ip_address INET, user_agent TEXT, location VARCHAR(120), expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT chk_device_type CHECK (device_type IN ('web','desktop','ios','android','bot')) ); CREATE INDEX idx_sessions_user ON auth.sessions(user_id) WHERE revoked_at IS NULL; CREATE INDEX idx_sessions_expires ON auth.sessions(expires_at) WHERE revoked_at IS NULL; πŸ”Ή Tabla: two_factor_auth CREATE TABLE auth.two_factor_auth ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID UNIQUE NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, secret VARCHAR(255) NOT NULL, -- TOTP secret (encrypted) backup_codes JSONB, -- Codigos de respaldo (hashed) enabled BOOLEAN DEFAULT FALSE, enabled_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); πŸ”Ή Tabla: password_resets CREATE TABLE auth.password_resets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, token_hash VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMPTZ NOT NULL, used_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_password_resets_user ON auth.password_resets(user_id) WHERE used_at IS NULL; 3.3 ESQUEMA: WORKSPACE (Espacios de Trabajo) πŸ”Ή Tabla: workspaces CREATE TABLE workspace.workspaces ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(80) NOT NULL, domain VARCHAR(50) UNIQUE NOT NULL, -- miempresa.chat.com logo_url VARCHAR(500), description TEXT, plan VARCHAR(20) DEFAULT 'free', -- free, pro, business, enterprise plan_expires TIMESTAMPTZ, max_members INTEGER DEFAULT 100, max_file_size BIGINT DEFAULT 104857600, -- 100MB default message_retention_days INTEGER, -- NULL = ilimitado file_retention_days INTEGER, allow_create_channels BOOLEAN DEFAULT TRUE, allow_custom_emoji BOOLEAN DEFAULT TRUE, allow_shared_channels BOOLEAN DEFAULT FALSE, enforce_2fa BOOLEAN DEFAULT FALSE, sso_required BOOLEAN DEFAULT FALSE, created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT chk_plan CHECK (plan IN ('free','pro','business','enterprise')) ); CREATE INDEX idx_workspaces_domain ON workspace.workspaces(domain); πŸ”Ή Tabla: workspace_members CREATE TABLE workspace.workspace_members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, role VARCHAR(20) DEFAULT 'member', -- owner, admin, member, guest guest_type VARCHAR(20), -- single_channel, multi_channel (si es guest) display_name VARCHAR(80), -- Override de nombre dentro del workspace title VARCHAR(120), -- Override de tΓ­tulo joined_at TIMESTAMPTZ DEFAULT NOW(), invited_by UUID REFERENCES auth.users(id), deactivated_at TIMESTAMPTZ, deactivated_by UUID REFERENCES auth.users(id), UNIQUE(workspace_id, user_id), CONSTRAINT chk_role CHECK (role IN ('owner','admin','member','guest')), CONSTRAINT chk_guest_type CHECK (guest_type IN ('single_channel','multi_channel') OR role != 'guest') ); CREATE INDEX idx_workspace_members_workspace ON workspace.workspace_members(workspace_id); CREATE INDEX idx_workspace_members_user ON workspace.workspace_members(user_id); CREATE INDEX idx_workspace_members_role ON workspace.workspace_members(workspace_id, role); πŸ”Ή Tabla: workspace_invitations CREATE TABLE workspace.workspace_invitations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, email VARCHAR(255) NOT NULL, role VARCHAR(20) DEFAULT 'member', token VARCHAR(255) NOT NULL UNIQUE, invited_by UUID NOT NULL REFERENCES auth.users(id), accepted_at TIMESTAMPTZ, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT chk_inv_role CHECK (role IN ('owner','admin','member','guest')) ); CREATE INDEX idx_invitations_workspace ON workspace.workspace_invitations(workspace_id); CREATE INDEX idx_invitations_email ON workspace.workspace_invitations(email) WHERE accepted_at IS NULL; πŸ”Ή Tabla: user_groups CREATE TABLE workspace.user_groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, name VARCHAR(80) NOT NULL, -- @marketing, @engineering handle VARCHAR(50) NOT NULL, -- marketing, engineering description TEXT, is_auto BOOLEAN DEFAULT FALSE, -- Auto-generado por dept created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(workspace_id, handle) ); CREATE INDEX idx_user_groups_workspace ON workspace.user_groups(workspace_id); πŸ”Ή Tabla: user_group_members CREATE TABLE workspace.user_group_members ( user_group_id UUID NOT NULL REFERENCES workspace.user_groups(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, added_at TIMESTAMPTZ DEFAULT NOW(), added_by UUID REFERENCES auth.users(id), PRIMARY KEY(user_group_id, user_id) ); πŸ”Ή Tabla: custom_emojis CREATE TABLE workspace.custom_emojis ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, name VARCHAR(50) NOT NULL, -- :parrot: image_url VARCHAR(500) NOT NULL, aliases VARCHAR(255)[], -- Nombres alternativos created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(workspace_id, name) ); CREATE INDEX idx_custom_emojis_workspace ON workspace.custom_emojis(workspace_id); πŸ”Ή Tabla: workspace_settings CREATE TABLE workspace.workspace_settings ( workspace_id UUID PRIMARY KEY REFERENCES workspace.workspaces(id) ON DELETE CASCADE, default_channel VARCHAR(80) DEFAULT 'general', allowed_file_types VARCHAR(255)[], -- NULL = todos allowed_file_extensions VARCHAR(255)[], message_edit_window_minutes INTEGER DEFAULT 0, -- 0 = ilimitado allow_message_deletion BOOLEAN DEFAULT TRUE, default_retention_days INTEGER, require_link_previews BOOLEAN DEFAULT TRUE, allowed_auth_domains VARCHAR(255)[], -- dominios de email permitidos sidebar_sections JSONB DEFAULT '[]', custom_theme JSONB, announcement_channel VARCHAR(80), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); 3.4 ESQUEMA: MESSAGING (MensajerΓ­a - EL CORE) πŸ”Ή Tabla: channels CREATE TABLE messaging.channels ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, name VARCHAR(80) NOT NULL, handle VARCHAR(80) NOT NULL, -- Slug: marketing-team type VARCHAR(20) NOT NULL, -- public, private, group_dm, dm, shared topic TEXT, topic_set_by UUID REFERENCES auth.users(id), topic_set_at TIMESTAMPTZ, purpose TEXT, purpose_set_by UUID REFERENCES auth.users(id), purpose_set_at TIMESTAMPTZ, is_archived BOOLEAN DEFAULT FALSE, archived_at TIMESTAMPTZ, archived_by UUID REFERENCES auth.users(id), is_default BOOLEAN DEFAULT FALSE, is_read_only BOOLEAN DEFAULT FALSE, is_shared BOOLEAN DEFAULT FALSE, -- Slack Connect is_org_wide BOOLEAN DEFAULT FALSE, created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), last_message_at TIMESTAMPTZ, member_count INTEGER DEFAULT 0, UNIQUE(workspace_id, handle), CONSTRAINT chk_channel_type CHECK (type IN ('public','private','group_dm','dm','shared')) ); -- CRÍTICO: Este Γ­ndice se usa en casi todas las queries CREATE INDEX idx_channels_workspace ON messaging.channels(workspace_id, type); CREATE INDEX idx_channels_workspace_handle ON messaging.channels(workspace_id, handle); CREATE INDEX idx_channels_archived ON messaging.channels(workspace_id, is_archived); CREATE INDEX idx_channels_last_message ON messaging.channels(workspace_id, last_message_at DESC); πŸ”Ή Tabla: channel_members CREATE TABLE messaging.channel_members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES messaging.channels(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, workspace_id UUID NOT NULL, -- Denormalizado para queries rΓ‘pidas role VARCHAR(20) DEFAULT 'member', -- owner, admin, member notification_pref VARCHAR(20) DEFAULT 'all', -- all, mentions, nothing muted BOOLEAN DEFAULT FALSE, muted_until TIMESTAMPTZ, last_read_ts TIMESTAMPTZ, -- Cursor de lectura unread_count INTEGER DEFAULT 0, unread_mention_count INTEGER DEFAULT 0, joined_at TIMESTAMPTZ DEFAULT NOW(), invited_by UUID REFERENCES auth.users(id), left_at TIMESTAMPTZ, UNIQUE(channel_id, user_id), CONSTRAINT chk_notif_pref CHECK (notification_pref IN ('all','mentions','nothing')) ); -- CRÍTICO: Para listar canales de un usuario CREATE INDEX idx_channel_members_user ON messaging.channel_members(user_id, workspace_id) WHERE left_at IS NULL; -- CRÍTICO: Para listar miembros de un canal CREATE INDEX idx_channel_members_channel ON messaging.channel_members(channel_id) WHERE left_at IS NULL; -- Para sidebar con unread counts CREATE INDEX idx_channel_members_unread ON messaging.channel_members(user_id) WHERE unread_count > 0 AND left_at IS NULL; πŸ”Ή Tabla: messages (LA TABLA MÁS IMPORTANTE) CREATE TABLE messaging.messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL, -- Denormalizado para sharding channel_id UUID NOT NULL REFERENCES messaging.channels(id) ON DELETE CASCADE, user_id UUID REFERENCES auth.users(id), -- NULL si es mensaje del sistema bot_id UUID, -- Si fue enviado por un bot thread_id UUID, -- Si es reply, ID del mensaje padre thread_root_id UUID, -- Siempre apunta al mensaje raΓ­z del thread text TEXT, -- Texto plano (fallback) blocks JSONB, -- Block Kit: contenido rico attachments JSONB, -- Attachments legacy mentioned_users UUID[], -- IDs de usuarios mencionados mentioned_channels UUID[], -- Canales mencionados (#canal) mentioned_groups UUID[], -- Grupos mencionados (@grupo) link_previews JSONB, -- Unfurling de URLs is_edited BOOLEAN DEFAULT FALSE, edited_at TIMESTAMPTZ, edited_by UUID REFERENCES auth.users(id), is_deleted BOOLEAN DEFAULT FALSE, deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id), is_pinned BOOLEAN DEFAULT FALSE, is_system BOOLEAN DEFAULT FALSE, -- Mensaje del sistema (join, leave) system_type VARCHAR(50), -- join, leave, channel_archive, etc. is_scheduled BOOLEAN DEFAULT FALSE, scheduled_for TIMESTAMPTZ, reply_count INTEGER DEFAULT 0, reply_users_count INTEGER DEFAULT 0, reply_users UUID[], latest_reply_ts TIMESTAMPTZ, ts TIMESTAMPTZ DEFAULT NOW(), -- Timestamp del mensaje (puede diferir de created_at para scheduled) created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- CRÍTICOS: -- Sharding por channel_id (fase 2 con Citus) CREATE INDEX idx_messages_channel_ts ON messaging.messages(channel_id, ts DESC); CREATE INDEX idx_messages_workspace ON messaging.messages(workspace_id, ts DESC); CREATE INDEX idx_messages_thread ON messaging.messages(thread_root_id, ts ASC) WHERE thread_root_id IS NOT NULL; CREATE INDEX idx_messages_user ON messaging.messages(user_id, ts DESC) WHERE is_deleted = FALSE; CREATE INDEX idx_messages_scheduled ON messaging.messages(is_scheduled, scheduled_for) WHERE is_scheduled = TRUE; CREATE INDEX idx_messages_mentions ON messaging.messages USING GIN (mentioned_users); CREATE INDEX idx_messages_blocks ON messaging.messages USING GIN (blocks jsonb_path_ops); -- PARTICIONAMIENTO (Fase 2): particionar por mes para mantenimiento -- CREATE TABLE messaging.messages_y2025m01 PARTITION OF messaging.messages -- FOR VALUES FROM ('2025-01-01') TO ('2025-02-01'); πŸ”Ή Tabla: message_reactions CREATE TABLE messaging.message_reactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES messaging.messages(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, emoji VARCHAR(50) NOT NULL, -- :thumbsup: o unicode emoji emoji_type VARCHAR(20) DEFAULT 'unicode', -- unicode, custom custom_emoji_id UUID, -- Si es custom emoji created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(message_id, user_id, emoji) ); CREATE INDEX idx_reactions_message ON messaging.message_reactions(message_id); CREATE INDEX idx_reactions_user ON messaging.message_reactions(user_id); πŸ”Ή Tabla: message_pins CREATE TABLE messaging.message_pins ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES messaging.channels(id) ON DELETE CASCADE, message_id UUID NOT NULL REFERENCES messaging.messages(id) ON DELETE CASCADE, pinned_by UUID NOT NULL REFERENCES auth.users(id), pinned_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(channel_id, message_id) ); πŸ”Ή Tabla: channel_bookmarks CREATE TABLE messaging.channel_bookmarks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES messaging.channels(id) ON DELETE CASCADE, type VARCHAR(20) NOT NULL, -- link, channel, message title VARCHAR(120), url VARCHAR(500), -- Si es link target_channel_id UUID, -- Si es channel target_message_id UUID, -- Si es message emoji VARCHAR(50), order_index INTEGER DEFAULT 0, created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_bookmarks_channel ON messaging.channel_bookmarks(channel_id, order_index); πŸ”Ή Tabla: saved_messages (Items guardados personalmente) CREATE TABLE messaging.saved_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, message_id UUID NOT NULL REFERENCES messaging.messages(id) ON DELETE CASCADE, saved_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id, message_id) ); CREATE INDEX idx_saved_messages_user ON messaging.saved_messages(user_id, saved_at DESC); πŸ”Ή Tabla: scheduled_messages CREATE TABLE messaging.scheduled_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL, channel_id UUID NOT NULL REFERENCES messaging.channels(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id), text TEXT, blocks JSONB, scheduled_for TIMESTAMPTZ NOT NULL, status VARCHAR(20) DEFAULT 'pending', -- pending, sent, cancelled sent_message_id UUID, -- Referencia al mensaje cuando se envΓ­a created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT chk_sched_status CHECK (status IN ('pending','sent','cancelled')) ); CREATE INDEX idx_scheduled_pending ON messaging.scheduled_messages(scheduled_for) WHERE status = 'pending'; πŸ”Ή Tabla: typing_indicators (Redis - no en PostgreSQL) NOTA: Los typing indicators se manejan en Redis, NO en PostgreSQL: Redis key: typing:{channel_id}:{user_id} TTL: 5 segundos Value: timestamp Cada vez que un usuario escribe: SET typing:{channel_id}:{user_id} {timestamp} EX 5 Para saber quiΓ©n estΓ‘ escribiendo en un canal: SCAN typing:{channel_id}:* πŸ”Ή Tabla: shared_channels (Slack Connect) CREATE TABLE messaging.shared_channels ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES messaging.channels(id) ON DELETE CASCADE, source_workspace_id UUID NOT NULL, -- Workspace que originΓ³ el canal target_workspace_id UUID NOT NULL, -- Workspace con quien se comparte target_channel_name VARCHAR(80), -- Override de nombre en el workspace destino target_channel_topic TEXT, target_is_private BOOLEAN DEFAULT FALSE, status VARCHAR(20) DEFAULT 'pending', -- pending, accepted, rejected, revoked accepted_at TIMESTAMPTZ, accepted_by UUID REFERENCES auth.users(id), created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT chk_shared_status CHECK (status IN ('pending','accepted','rejected','revoked')) ); CREATE INDEX idx_shared_channels_source ON messaging.shared_channels(source_workspace_id); CREATE INDEX idx_shared_channels_target ON messaging.shared_channels(target_workspace_id); 3.5 ESQUEMA: FILES (Archivos) πŸ”Ή Tabla: files CREATE TABLE files.files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL, uploader_id UUID NOT NULL REFERENCES auth.users(id), filename VARCHAR(255) NOT NULL, file_type VARCHAR(50) NOT NULL, -- image, pdf, video, audio, code, doc, etc. file_extension VARCHAR(20), mime_type VARCHAR(100) NOT NULL, size_bytes BIGINT NOT NULL, storage_key VARCHAR(500) NOT NULL, -- Key en S3/MinIO storage_bucket VARCHAR(100) NOT NULL, storage_provider VARCHAR(20) DEFAULT 's3', -- s3, minio, local thumbnail_key VARCHAR(500), -- Key del thumbnail en S3 thumbnail_sizes JSONB, -- {"small": "key1", "medium": "key2"} image_width INTEGER, image_height INTEGER, duration_seconds FLOAT, -- Para audio/video page_count INTEGER, -- Para PDFs is_public BOOLEAN DEFAULT FALSE, public_url VARCHAR(500), is_deleted BOOLEAN DEFAULT FALSE, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_files_workspace ON files.files(workspace_id, created_at DESC); CREATE INDEX idx_files_uploader ON files.files(uploader_id, created_at DESC); CREATE INDEX idx_files_type ON files.files(workspace_id, file_type); πŸ”Ή Tabla: file_shares CREATE TABLE files.file_shares ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), file_id UUID NOT NULL REFERENCES files.files(id) ON DELETE CASCADE, channel_id UUID REFERENCES messaging.channels(id) ON DELETE CASCADE, message_id UUID REFERENCES messaging.messages(id) ON DELETE CASCADE, shared_by UUID NOT NULL REFERENCES auth.users(id), shared_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_file_shares_file ON files.file_shares(file_id); CREATE INDEX idx_file_shares_channel ON files.file_shares(channel_id); πŸ”Ή Tabla: file_comments CREATE TABLE files.file_comments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), file_id UUID NOT NULL REFERENCES files.files(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id), text TEXT NOT NULL, blocks JSONB, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), is_deleted BOOLEAN DEFAULT FALSE, deleted_at TIMESTAMPTZ ); CREATE INDEX idx_file_comments_file ON files.file_comments(file_id, created_at DESC); 3.6 ESQUEMA: PROJECTS (Lists, Canvases, Tasks) πŸ”Ή Tabla: lists CREATE TABLE workspace.lists ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, channel_id UUID REFERENCES messaging.channels(id) ON DELETE CASCADE, name VARCHAR(120) NOT NULL, description TEXT, view_type VARCHAR(20) DEFAULT 'table', -- table, board group_by_field VARCHAR(50), -- Campo para agrupar en board view created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_lists_workspace ON workspace.lists(workspace_id); CREATE INDEX idx_lists_channel ON workspace.lists(channel_id); πŸ”Ή Tabla: list_fields (Columnas personalizadas) CREATE TABLE workspace.list_fields ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), list_id UUID NOT NULL REFERENCES workspace.lists(id) ON DELETE CASCADE, name VARCHAR(80) NOT NULL, field_type VARCHAR(20) NOT NULL, -- text, number, checkbox, date, select, people options JSONB, -- Para select: opciones order_index INTEGER DEFAULT 0, is_required BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT chk_field_type CHECK (field_type IN ('text','number','checkbox','date','select','people','multi_select','url','email')) ); CREATE INDEX idx_list_fields_list ON workspace.list_fields(list_id, order_index); πŸ”Ή Tabla: list_items (Tareas/filas) CREATE TABLE workspace.list_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), list_id UUID NOT NULL REFERENCES workspace.lists(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, field_values JSONB NOT NULL DEFAULT '{}', -- {"Assignee": "uuid", "Due Date": "2025-01-15", "Status": "Done"} order_index INTEGER DEFAULT 0, created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), completed_at TIMESTAMPTZ ); CREATE INDEX idx_list_items_list ON workspace.list_items(list_id, order_index); CREATE INDEX idx_list_items_fields ON workspace.list_items USING GIN (field_values); πŸ”Ή Tabla: list_item_comments CREATE TABLE workspace.list_item_comments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), list_item_id UUID NOT NULL REFERENCES workspace.list_items(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id), text TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_list_item_comments_item ON workspace.list_item_comments(list_item_id, created_at DESC); πŸ”Ή Tabla: canvases (Documentos colaborativos) CREATE TABLE workspace.canvases ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, channel_id UUID REFERENCES messaging.channels(id) ON DELETE CASCADE, title VARCHAR(200) NOT NULL, content JSONB NOT NULL DEFAULT '{}', -- TipTap/ProseMirror document JSON document_id UUID NOT NULL, -- ID del documento TipTap version INTEGER DEFAULT 1, is_template BOOLEAN DEFAULT FALSE, template_category VARCHAR(50), created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), last_edited_by UUID REFERENCES auth.users(id) ); CREATE INDEX idx_canvases_workspace ON workspace.canvases(workspace_id); CREATE INDEX idx_canvases_channel ON workspace.canvases(channel_id); πŸ”Ή Tabla: canvas_collaborators CREATE TABLE workspace.canvas_collaborators ( canvas_id UUID NOT NULL REFERENCES workspace.canvases(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, permission VARCHAR(20) DEFAULT 'edit', -- edit, view added_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY(canvas_id, user_id) ); πŸ”Ή Tabla: templates CREATE TABLE workspace.templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID REFERENCES workspace.workspaces(id) ON DELETE CASCADE, -- NULL si es global name VARCHAR(120) NOT NULL, description TEXT, category VARCHAR(50), -- onboarding, project, standup, etc. content JSONB NOT NULL, -- Estructura del template type VARCHAR(20) NOT NULL, -- channel_setup, canvas, list, workflow created_by UUID REFERENCES auth.users(id), is_custom BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_templates_workspace ON workspace.templates(workspace_id, category); 3.7 ESQUEMA: AUTOMATION (Workflows, Integraciones, Bots) πŸ”Ή Tabla: workflows CREATE TABLE automation.workflows ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, name VARCHAR(120) NOT NULL, description TEXT, status VARCHAR(20) DEFAULT 'draft', -- draft, active, paused, archived trigger_type VARCHAR(50) NOT NULL, trigger_config JSONB NOT NULL, -- ConfiguraciΓ³n del trigger steps JSONB NOT NULL DEFAULT '[]', -- Array de pasos variables JSONB DEFAULT '{}', -- Variables del workflow execution_count INTEGER DEFAULT 0, failure_count INTEGER DEFAULT 0, last_executed TIMESTAMPTZ, created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT chk_workflow_status CHECK (status IN ('draft','active','paused','archived')) ); CREATE INDEX idx_workflows_workspace ON automation.workflows(workspace_id, status); πŸ”Ή Tabla: workflow_executions CREATE TABLE automation.workflow_executions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workflow_id UUID NOT NULL REFERENCES automation.workflows(id) ON DELETE CASCADE, trigger_data JSONB, -- Datos que dispararon el workflow context JSONB, -- Variables en ejecuciΓ³n current_step INTEGER DEFAULT 0, status VARCHAR(20) DEFAULT 'running', -- running, completed, failed, cancelled error_message TEXT, started_at TIMESTAMPTZ DEFAULT NOW(), completed_at TIMESTAMPTZ ); CREATE INDEX idx_workflow_exec_workflow ON automation.workflow_executions(workflow_id, started_at DESC); CREATE INDEX idx_workflow_exec_status ON automation.workflow_executions(status) WHERE status = 'running'; πŸ”Ή Tabla: apps (Integraciones instaladas) CREATE TABLE automation.apps ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, app_name VARCHAR(80) NOT NULL, -- Google Drive, GitHub, etc. app_slug VARCHAR(80) NOT NULL, description TEXT, icon_url VARCHAR(500), developer_name VARCHAR(120), is_internal BOOLEAN DEFAULT FALSE, -- TRUE si es app custom del workspace scopes TEXT[], -- Permisos solicitados oauth_token_encrypted TEXT, -- Token encriptado oauth_refresh_token_encrypted TEXT, installed_by UUID NOT NULL REFERENCES auth.users(id), installed_at TIMESTAMPTZ DEFAULT NOW(), is_active BOOLEAN DEFAULT TRUE, config JSONB DEFAULT '{}', UNIQUE(workspace_id, app_slug) ); CREATE INDEX idx_apps_workspace ON automation.apps(workspace_id, is_active); πŸ”Ή Tabla: bot_users CREATE TABLE automation.bot_users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, app_id UUID REFERENCES automation.apps(id) ON DELETE CASCADE, name VARCHAR(80) NOT NULL, display_name VARCHAR(80) NOT NULL, avatar_url VARCHAR(500), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_bot_users_workspace ON automation.bot_users(workspace_id); πŸ”Ή Tabla: slash_commands CREATE TABLE automation.slash_commands ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, app_id UUID REFERENCES automation.apps(id) ON DELETE CASCADE, command VARCHAR(50) NOT NULL, -- /ticket description TEXT, usage_hint VARCHAR(200), url VARCHAR(500), -- Webhook URL created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(workspace_id, command) ); πŸ”Ή Tabla: incoming_webhooks CREATE TABLE automation.incoming_webhooks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, app_id UUID REFERENCES automation.apps(id) ON DELETE CASCADE, channel_id UUID REFERENCES messaging.channels(id), webhook_url VARCHAR(500) UNIQUE NOT NULL, -- URL ΓΊnica del webhook webhook_token VARCHAR(255) NOT NULL, created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), is_active BOOLEAN DEFAULT TRUE ); πŸ”Ή Tabla: event_subscriptions CREATE TABLE automation.event_subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, app_id UUID REFERENCES automation.apps(id) ON DELETE CASCADE, callback_url VARCHAR(500) NOT NULL, event_types TEXT[], -- ['message.channels', 'team_join', ...] secret VARCHAR(255) NOT NULL, -- Para HMAC signing is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW() ); 3.8 ESQUEMA: HUDDLES (Llamadas Audio/Video) πŸ”Ή Tabla: huddles CREATE TABLE workspace.huddles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL, channel_id UUID NOT NULL REFERENCES messaging.channels(id) ON DELETE CASCADE, started_by UUID NOT NULL REFERENCES auth.users(id), status VARCHAR(20) DEFAULT 'active', -- active, ended room_id VARCHAR(255), -- ID del room en Mediasoup/LiveKit is_video BOOLEAN DEFAULT FALSE, started_at TIMESTAMPTZ DEFAULT NOW(), ended_at TIMESTAMPTZ, participant_count INTEGER DEFAULT 0, max_participants INTEGER DEFAULT 0 ); CREATE INDEX idx_huddles_channel_active ON workspace.huddles(channel_id) WHERE status = 'active'; πŸ”Ή Tabla: huddle_participants CREATE TABLE workspace.huddle_participants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), huddle_id UUID NOT NULL REFERENCES workspace.huddles(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id), joined_at TIMESTAMPTZ DEFAULT NOW(), left_at TIMESTAMPTZ, had_video BOOLEAN DEFAULT FALSE, had_audio BOOLEAN DEFAULT TRUE, was_screensharing BOOLEAN DEFAULT FALSE, UNIQUE(huddle_id, user_id) ); CREATE INDEX idx_huddle_participants_huddle ON workspace.huddle_participants(huddle_id) WHERE left_at IS NULL; πŸ”Ή Tabla: huddle_notes (IA generadas) CREATE TABLE workspace.huddle_notes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), huddle_id UUID NOT NULL REFERENCES workspace.huddles(id) ON DELETE CASCADE, transcript TEXT, -- TranscripciΓ³n completa summary TEXT, -- Resumen IA action_items JSONB, -- [{item: "...", assignee: "uuid"}] key_takeaways TEXT[], generated_at TIMESTAMPTZ DEFAULT NOW() ); 3.9 ESQUEMA: AI (Inteligencia Artificial) πŸ”Ή Tabla: ai_embeddings (Vector DB para RAG) -- Requiere extensiΓ³n pgvector: CREATE EXTENSION vector; CREATE TABLE ai.message_embeddings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL, message_id UUID NOT NULL REFERENCES messaging.messages(id) ON DELETE CASCADE, channel_id UUID NOT NULL, content TEXT, -- Texto que fue embebido embedding vector(1536), -- OpenAI text-embedding-3-small = 1536 dims model VARCHAR(50) DEFAULT 'text-embedding-3-small', created_at TIMESTAMPTZ DEFAULT NOW() ); -- CRÍTICO: Para bΓΊsqueda semΓ‘ntica CREATE INDEX idx_embeddings_vector ON ai.message_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); CREATE INDEX idx_embeddings_workspace ON ai.message_embeddings(workspace_id); CREATE INDEX idx_embeddings_channel ON ai.message_embeddings(channel_id); πŸ”Ή Tabla: ai_summaries (ResΓΊmenes generados) CREATE TABLE ai.ai_summaries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL, type VARCHAR(30) NOT NULL, -- channel, thread, dm, huddle, file, recap target_id UUID NOT NULL, -- ID del canal/thread/huddle/file summary TEXT NOT NULL, key_points TEXT[], action_items JSONB, period_start TIMESTAMPTZ, period_end TIMESTAMPTZ, generated_for UUID NOT NULL REFERENCES auth.users(id), -- Usuario que solicitΓ³ model VARCHAR(50) DEFAULT 'gpt-4o', tokens_used INTEGER, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_ai_summaries_workspace ON ai.ai_summaries(workspace_id, type, created_at DESC); CREATE INDEX idx_ai_summaries_target ON ai.ai_summaries(target_id, created_at DESC); πŸ”Ή Tabla: ai_conversations (Chat con Slackbot) CREATE TABLE ai.ai_conversations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, messages JSONB NOT NULL DEFAULT '[]', -- [{role: "user", content: "..."}, {role: "assistant", content: "..."}] context_refs UUID[], -- IDs de mensajes usados como contexto created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_ai_conversations_user ON ai.ai_conversations(user_id, updated_at DESC); πŸ”Ή Tabla: ai_search_cache CREATE TABLE ai.ai_search_cache ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL, user_id UUID NOT NULL, query TEXT NOT NULL, answer TEXT NOT NULL, source_message_ids UUID[], -- Mensajes usados como fuente model VARCHAR(50), created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_ai_search_workspace ON ai.ai_search_cache(workspace_id, created_at DESC); 3.10 ESQUEMA: NOTIFICATIONS (Notificaciones) πŸ”Ή Tabla: notification_preferences CREATE TABLE workspace.notification_preferences ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, channel_id UUID REFERENCES messaging.channels(id) ON DELETE CASCADE, -- NULL = default del workspace notify_mentions BOOLEAN DEFAULT TRUE, notify_all_messages BOOLEAN DEFAULT FALSE, notify_threads VARCHAR(20) DEFAULT 'all', -- all, mentions, nothing notify_dms BOOLEAN DEFAULT TRUE, notify_huddles BOOLEAN DEFAULT TRUE, push_enabled BOOLEAN DEFAULT TRUE, email_enabled BOOLEAN DEFAULT TRUE, email_frequency VARCHAR(20) DEFAULT 'hourly', -- immediate, hourly, daily, weekly desktop_enabled BOOLEAN DEFAULT TRUE, mobile_enabled BOOLEAN DEFAULT TRUE, sound_enabled BOOLEAN DEFAULT TRUE, dnd_enabled BOOLEAN DEFAULT FALSE, dnd_start TIME, -- 22:00 dnd_end TIME, -- 08:00 dnd_timezone VARCHAR(50), keywords TEXT[], -- Palabras que disparan notificaciΓ³n created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_notif_prefs_user_workspace ON workspace.notification_preferences(user_id, workspace_id); CREATE INDEX idx_notif_prefs_channel ON workspace.notification_preferences(channel_id); πŸ”Ή Tabla: push_tokens CREATE TABLE workspace.push_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, platform VARCHAR(20) NOT NULL, -- ios, android, web, desktop token VARCHAR(500) NOT NULL, device_id VARCHAR(255), app_version VARCHAR(50), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT chk_platform CHECK (platform IN ('ios','android','web','desktop')) ); CREATE INDEX idx_push_tokens_user ON workspace.push_tokens(user_id) WHERE is_active = TRUE; πŸ”Ή Tabla: notification_queue (Redis - no PostgreSQL) NOTA: La cola de notificaciones se maneja en Redis (BullMQ): Queue: notifications Jobs: β”œβ”€β”€ {user_id, type: 'mention', message_id, channel_id, timestamp} β”œβ”€β”€ {user_id, type: 'dm', message_id, timestamp} β”œβ”€β”€ {user_id, type: 'thread_reply', message_id, timestamp} └── {user_id, type: 'channel_message', message_id, channel_id} Procesador: β”œβ”€β”€ Verifica preferencias del usuario β”œβ”€β”€ Verifica DND β”œβ”€β”€ Determina canales de entrega (push, email, desktop) β”œβ”€β”€ Batching: si hay mΓΊltiples, agrupa en una └── EnvΓ­a via FCM/APNs/Web Push/Email 3.11 ESQUEMA: ANALYTICS Y AUDIT πŸ”Ή Tabla: audit_logs CREATE TABLE analytics.audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL, actor_id UUID REFERENCES auth.users(id), actor_type VARCHAR(20) DEFAULT 'user', -- user, admin, system, app action VARCHAR(80) NOT NULL, -- user.invite, channel.create, message.delete entity_type VARCHAR(50), -- user, channel, message, workspace entity_id UUID, details JSONB, -- Detalles especΓ­ficos del evento ip_address INET, user_agent TEXT, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_audit_workspace ON analytics.audit_logs(workspace_id, created_at DESC); CREATE INDEX idx_audit_actor ON analytics.audit_logs(actor_id, created_at DESC); CREATE INDEX idx_audit_action ON analytics.audit_logs(workspace_id, action); CREATE INDEX idx_audit_entity ON analytics.audit_logs(entity_type, entity_id); πŸ”Ή Tabla: usage_stats (Agregados diarios) CREATE TABLE analytics.usage_stats ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL, stat_date DATE NOT NULL, daily_active_users INTEGER DEFAULT 0, weekly_active_users INTEGER DEFAULT 0, monthly_active_users INTEGER DEFAULT 0, messages_sent INTEGER DEFAULT 0, channels_active INTEGER DEFAULT 0, huddles_count INTEGER DEFAULT 0, huddle_minutes INTEGER DEFAULT 0, files_uploaded INTEGER DEFAULT 0, apps_active INTEGER DEFAULT 0, workflows_executed INTEGER DEFAULT 0, ai_queries INTEGER DEFAULT 0, UNIQUE(workspace_id, stat_date) ); CREATE INDEX idx_usage_workspace_date ON analytics.usage_stats(workspace_id, stat_date DESC); πŸ”Ή Tabla: message_activity (Tracking por canal - para analytics) CREATE TABLE analytics.message_activity ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL, channel_id UUID NOT NULL, stat_date DATE NOT NULL, messages_count INTEGER DEFAULT 0, unique_posters INTEGER DEFAULT 0, reactions_count INTEGER DEFAULT 0, thread_replies INTEGER DEFAULT 0, UNIQUE(channel_id, stat_date) ); CREATE INDEX idx_msg_activity_workspace ON analytics.message_activity(workspace_id, stat_date DESC); 3.12 ESQUEMA: SECURITY (Seguridad y Compliance) πŸ”Ή Tabla: data_retention_policies CREATE TABLE workspace.data_retention_policies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, scope VARCHAR(20) NOT NULL, -- workspace, channel channel_id UUID REFERENCES messaging.channels(id) ON DELETE CASCADE, message_retention_days INTEGER, -- NULL = ilimitado file_retention_days INTEGER, retain_threads BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); πŸ”Ή Tabla: legal_holds CREATE TABLE workspace.legal_holds ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, name VARCHAR(120) NOT NULL, description TEXT, user_ids UUID[], -- Usuarios en hold channel_ids UUID[], -- Canales en hold start_date TIMESTAMPTZ DEFAULT NOW(), end_date TIMESTAMPTZ, created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW() ); πŸ”Ή Tabla: information_barriers CREATE TABLE workspace.information_barriers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace.workspaces(id) ON DELETE CASCADE, name VARCHAR(120) NOT NULL, group_a_type VARCHAR(20) NOT NULL, -- user_group, channel group_a_ids UUID[] NOT NULL, group_b_type VARCHAR(20) NOT NULL, group_b_ids UUID[] NOT NULL, created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), is_active BOOLEAN DEFAULT TRUE ); 3.13 ESTRATEGIA DE ÍNDICES Y OPTIMIZACIΓ“N πŸ”Ή Índices CrΓ­ticos (Hot Path) 1. CARGAR SIDEBAR DE CANALES (por usuario): Query: SELECT c.*, cm.unread_count, cm.last_read_ts FROM channels c JOIN channel_members cm ON cm.channel_id = c.id WHERE cm.user_id = ? AND cm.workspace_id = ? AND cm.left_at IS NULL ORDER BY c.last_message_at DESC Índice: idx_channel_members_user (user_id, workspace_id) WHERE left_at IS NULL βœ“ Índice: idx_channels_last_message (workspace_id, last_message_at DESC) βœ“ 2. CARGAR MENSAJES DE UN CANAL (paginaciΓ³n): Query: SELECT * FROM messages WHERE channel_id = ? AND is_deleted = FALSE ORDER BY ts DESC LIMIT 50 OFFSET ? Índice: idx_messages_channel_ts (channel_id, ts DESC) βœ“ 3. BUSCAR MENSAJES: Query: Via Elasticsearch/Meilisearch (no PostgreSQL) PostgreSQL fallback: idx_messages_workspace (workspace_id, ts DESC) βœ“ 4. THREADS: Query: SELECT * FROM messages WHERE thread_root_id = ? ORDER BY ts ASC Índice: idx_messages_thread (thread_root_id, ts ASC) βœ“ 5. PRESENCIA: Redis: presence:{user_id} β†’ TTL 60s 6. TYPING: Redis: typing:{channel_id}:{user_id} β†’ TTL 5s πŸ”Ή Estrategia de Particionamiento (Fase 2) -- Particionar messages por mes (para retenciΓ³n y performance) CREATE TABLE messaging.messages ( -- ... todos los campos ... ) PARTITION BY RANGE (ts); -- Particiones mensuales CREATE TABLE messaging.messages_2025_01 PARTITION OF messaging.messages FOR VALUES FROM ('2025-01-01') TO ('2025-02-01'); CREATE TABLE messaging.messages_2025_02 PARTITION OF messaging.messages FOR VALUES FROM ('2025-02-01') TO ('2025-03-01'); -- Auto-crear particiones con pg_partman -- SELECT partman.create_parent('messaging.messages', 'ts', 'native', 'monthly'); πŸ”Ή Row Level Security (Multi-tenant) -- Habilitar RLS en todas las tablas con workspace_id ALTER TABLE messaging.channels ENABLE ROW LEVEL SECURITY; ALTER TABLE messaging.messages ENABLE ROW LEVEL SECURITY; ALTER TABLE messaging.channel_members ENABLE ROW LEVEL SECURITY; -- ... etc -- PolΓ­tica: un usuario solo puede ver datos de sus workspaces CREATE POLICY workspace_isolation ON messaging.channels USING (workspace_id IN ( SELECT workspace_id FROM workspace.workspace_members WHERE user_id = current_setting('app.current_user_id')::uuid AND deactivated_at IS NULL )); 3.14 REDIS: ESTRUCTURAS DE DATOS EN MEMORIA πŸ”Ή Keys principales en Redis: # PRESENCIA presence:{user_id} β†’ STRING "online" | TTL 60s presence:workspace:{workspace_id} β†’ SET de user_ids online presence:channel:{channel_id} β†’ SET de user_ids online en canal # TYPING INDICATORS typing:{channel_id}:{user_id} β†’ STRING timestamp | TTL 5s # UNREAD COUNTS (cache rΓ‘pido) unread:{user_id}:{channel_id} β†’ INTEGER count | TTL 24h # SESSIONS (active WebSocket connections) ws:connected:{user_id} β†’ SET de server_ids donde tiene conexiones ws:server:{server_id}:channels β†’ SET de channel_ids suscritos en ese server # RATE LIMITING ratelimit:api:{user_id}:{endpoint} β†’ STRING counter | TTL 60s ratelimit:ws:{user_id}:messages β†’ STRING counter | TTL 60s # PUB/SUB (para broadcasting entre WebSocket servers) channel:{channel_id} β†’ PUBSUB channel (publicar mensajes aquΓ­) presence:updates β†’ PUBSUB channel user:{user_id} β†’ PUBSUB channel (eventos personales) # JOB QUEUES (BullMQ) bull:notifications β†’ Queue de notificaciones pendientes bull:workflows β†’ Queue de ejecuciones de workflow bull:embeddings β†’ Queue de generaciΓ³n de embeddings bull:link_unfurling β†’ Queue de unfurling de URLs bull:file_processing β†’ Queue de procesamiento de archivos bull:search_indexing β†’ Queue de indexaciΓ³n a Meilisearch/ES bull:scheduled_messages β†’ Queue de mensajes programados bull:email β†’ Queue de envΓ­o de emails # CACHE cache:user:{user_id} β†’ HASH con datos del usuario | TTL 5min cache:channel:{channel_id} β†’ HASH con datos del canal | TTL 5min cache:workspace:{workspace_id} β†’ HASH con datos del workspace | TTL 10min cache:workspace_members:{workspace_id}β†’ SET de user_ids | TTL 5min cache:channel_members:{channel_id} β†’ SET de user_ids | TTL 5min cache:user_channels:{user_id} β†’ SET de channel_ids | TTL 5min # ONLINE CONNECTIONS COUNTER stats:online_users:total β†’ INTEGER (total conexiones globales) stats:online_users:workspace:{ws_id} β†’ INTEGER (conexiones por workspace) 3.15 RESUMEN DE TABLAS (TOTAL: 40+ TABLAS) Esquema Tablas Cantidad auth users, user_identities, sessions, two_factor_auth, password_resets 5 workspace workspaces, workspace_members, workspace_invitations, user_groups, user_group_members, custom_emojis, workspace_settings, lists, list_fields, list_items, list_item_comments, canvases, canvas_collaborators, templates, notification_preferences, push_tokens, huddles, huddle_participants, huddle_notes, data_retention_policies, legal_holds, information_barriers 22 messaging channels, channel_members, messages, message_reactions, message_pins, channel_bookmarks, saved_messages, scheduled_messages, shared_channels 9 files files, file_shares, file_comments 3 automation workflows, workflow_executions, apps, bot_users, slash_commands, incoming_webhooks, event_subscriptions 7 analytics audit_logs, usage_stats, message_activity 3 ai message_embeddings, ai_summaries, ai_conversations, ai_search_cache 4 TOTAL 53 tablas πŸ“Œ PARTE 4: BACKEND COMPLETO β€” APIs, WEBSOCKET, REAL-TIME Y SERVICIOS 4.1 ARQUITECTURA DEL BACKEND (NestJS) πŸ”Ή Estructura de Proyectos backend/ β”œβ”€β”€ apps/ β”‚ β”œβ”€β”€ api/ β†’ API Gateway + REST endpoints β”‚ β”‚ β”œβ”€β”€ src/ β”‚ β”‚ β”‚ β”œβ”€β”€ modules/ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ auth/ β†’ Login, register, SSO, 2FA β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ users/ β†’ CRUD usuarios, perfil, status β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ workspaces/ β†’ CRUD workspaces, miembros β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ channels/ β†’ CRUD canales, membresΓ­a β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ messages/ β†’ Enviar, editar, borrar, threads β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ files/ β†’ Upload, download, preview β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ search/ β†’ BΓΊsqueda global β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ huddles/ β†’ Llamadas audio/video β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ lists/ β†’ Lists, tasks, kanban β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ canvases/ β†’ Documentos colaborativos β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ workflows/ β†’ Automatizaciones β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ integrations/ β†’ Apps, webhooks, bots β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ notifications/ β†’ Preferencias, push β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ admin/ β†’ Analytics, audit, gestiΓ³n β”‚ β”‚ β”‚ β”‚ └── ai/ β†’ Slackbot, summaries, search IA β”‚ β”‚ β”‚ β”œβ”€β”€ common/ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ decorators/ β†’ @CurrentUser, @Workspace, @Roles β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ guards/ β†’ JwtGuard, WorkspaceGuard, RoleGuard β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ interceptors/ β†’ Logging, Transform, RateLimit β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ filters/ β†’ ExceptionFilter global β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pipes/ β†’ ValidationPipe custom β”‚ β”‚ β”‚ β”‚ └── middleware/ β†’ RequestId, WorkspaceContext β”‚ β”‚ β”‚ β”œβ”€β”€ config/ β†’ ConfiguraciΓ³n por env β”‚ β”‚ β”‚ β”œβ”€β”€ main.ts β”‚ β”‚ β”‚ └── app.module.ts β”‚ β”‚ β”œβ”€β”€ package.json β”‚ β”‚ └── tsconfig.json β”‚ β”‚ β”‚ β”œβ”€β”€ realtime/ β†’ WebSocket Gateway β”‚ β”‚ β”œβ”€β”€ src/ β”‚ β”‚ β”‚ β”œβ”€β”€ gateways/ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ chat.gateway.ts β†’ Eventos de mensajerΓ­a β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ presence.gateway.ts β†’ Presencia y status β”‚ β”‚ β”‚ β”‚ └── huddle.gateway.ts β†’ SeΓ±alizaciΓ³n WebRTC β”‚ β”‚ β”‚ β”œβ”€β”€ services/ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ connection.service.ts β†’ GestiΓ³n de conexiones β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pubsub.service.ts β†’ Redis Pub/Sub β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ presence.service.ts β†’ Tracking presencia β”‚ β”‚ β”‚ β”‚ └── broadcast.service.ts β†’ Broadcasting β”‚ β”‚ β”‚ β”œβ”€β”€ events/ β†’ DefiniciΓ³n de eventos β”‚ β”‚ β”‚ β”‚ └── events.ts β”‚ β”‚ β”‚ └── main.ts β”‚ β”‚ └── package.json β”‚ β”‚ β”‚ └── workers/ β†’ Background job processors β”‚ β”œβ”€β”€ src/ β”‚ β”‚ β”œβ”€β”€ processors/ β”‚ β”‚ β”‚ β”œβ”€β”€ notifications.processor.ts β”‚ β”‚ β”‚ β”œβ”€β”€ search-indexer.processor.ts β”‚ β”‚ β”‚ β”œβ”€β”€ embeddings.processor.ts β”‚ β”‚ β”‚ β”œβ”€β”€ link-unfurl.processor.ts β”‚ β”‚ β”‚ β”œβ”€β”€ file-processing.processor.ts β”‚ β”‚ β”‚ β”œβ”€β”€ scheduled-messages.processor.ts β”‚ β”‚ β”‚ β”œβ”€β”€ workflow-executor.processor.ts β”‚ β”‚ β”‚ └── ai-summaries.processor.ts β”‚ β”‚ └── main.ts β”‚ └── package.json β”‚ β”œβ”€β”€ libs/ β”‚ β”œβ”€β”€ database/ β†’ Prisma schema, migrations, seeds β”‚ β”œβ”€β”€ redis/ β†’ Redis client, pub/sub, cache helpers β”‚ β”œβ”€β”€ types/ β†’ TypeScript types compartidos β”‚ β”œβ”€β”€ utils/ β†’ Helpers (crypto, date, validation) β”‚ β”œβ”€β”€ constants/ β†’ Constantes y enums β”‚ └── dtos/ β†’ Data Transfer Objects compartidos β”‚ β”œβ”€β”€ docker-compose.yml β”œβ”€β”€ package.json β†’ Workspace root (monorepo) └── turbo.json β†’ Turborepo config 4.2 SISTEMA DE AUTENTICACIΓ“N πŸ”Ή JWT + Refresh Token Strategy Flujo de autenticaciΓ³n: 1. LOGIN: POST /api/v1/auth/login β†’ { email, password } ← { access_token (15min), refresh_token (30d), user } 2. REQUESTS AUTENTICADOS: Authorization: Bearer {access_token} Header: X-Workspace-Id: {workspace_id} 3. TOKEN REFRESH: POST /api/v1/auth/refresh β†’ { refresh_token } ← { access_token, refresh_token } (rotaciΓ³n) 4. LOGOUT: POST /api/v1/auth/logout β†’ Revoca refresh token + invalida sesiΓ³n πŸ”Ή Middleware de AutenticaciΓ³n // apps/api/src/common/guards/jwt.guard.ts @Injectable() export class JwtAuthGuard implements CanActivate { constructor(private jwtService: JwtService) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const token = this.extractToken(request); if (!token) { throw new UnauthorizedException('No token provided'); } try { const payload = await this.jwtService.verifyAsync(token, { secret: process.env.JWT_SECRET, }); request.user = payload; // { userId, email, iat, exp } } catch { throw new UnauthorizedException('Invalid or expired token'); } return true; } private extractToken(request: Request): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; } } πŸ”Ή Workspace Context Middleware // apps/api/src/common/middleware/workspace-context.middleware.ts @Injectable() export class WorkspaceContextMiddleware implements NestMiddleware { constructor(private workspaceMemberService: WorkspaceMemberService) {} async use(req: Request, res: Response, next: NextFunction) { const workspaceId = req.headers['x-workspace-id'] as string; const userId = req.user?.userId; if (workspaceId && userId) { // Verificar que el usuario es miembro del workspace const membership = await this.workspaceMemberService.findByUserAndWorkspace( userId, workspaceId ); if (!membership || membership.deactivatedAt) { throw new ForbiddenException('Not a member of this workspace'); } req.workspace = { id: workspaceId, role: membership.role, membershipId: membership.id, }; } next(); } } πŸ”Ή Decoradores Personalizados // apps/api/src/common/decorators/current-user.decorator.ts export const CurrentUser = createParamDecorator( (data: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const user = request.user; return data ? user?.[data] : user; } ); // apps/api/src/common/decorators/workspace.decorator.ts export const Workspace = createParamDecorator( (data: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const workspace = request.workspace; return data ? workspace?.[data] : workspace; } ); // apps/api/src/common/decorators/roles.decorator.ts export const Roles = (...roles: string[]) => SetMetadata('roles', roles); // Uso en controllers: @Get('channels') @Roles('owner', 'admin', 'member') async getChannels(@CurrentUser('userId') userId: string, @Workspace('id') wsId: string) { return this.channelService.findAll(userId, wsId); } 4.3 APIs REST COMPLETAS (TODOS LOS ENDPOINTS) πŸ”Ή A. AUTH MODULE // ============================================ // POST /api/v1/auth/register // ============================================ // Request: { "email": "user@company.com", "password": "SecurePass123!", "display_name": "John Doe", "workspace_name": "My Company" // Opcional: crea workspace tambiΓ©n } // Response 201: { "user": { "id": "uuid", "email": "...", "display_name": "..." }, "workspace": { "id": "uuid", "name": "My Company", "domain": "my-company" }, "access_token": "eyJ...", "refresh_token": "eyJ..." } // ============================================ // POST /api/v1/auth/login // ============================================ // Request: { "email": "user@company.com", "password": "SecurePass123!" } // Response 200: { "user": { "id": "uuid", "email": "...", "display_name": "..." }, "workspaces": [ { "id": "uuid", "name": "My Company", "domain": "my-company", "role": "owner" }, { "id": "uuid", "name": "Client Project", "domain": "client-proj", "role": "member" } ], "access_token": "eyJ...", "refresh_token": "eyJ..." } // ============================================ // POST /api/v1/auth/refresh // ============================================ // Request: { "refresh_token": "eyJ..." } // Response: { "access_token": "eyJ...", "refresh_token": "eyJ..." } // ============================================ // POST /api/v1/auth/logout // ============================================ // Headers: Authorization: Bearer {token} // Response: 204 No Content // ============================================ // POST /api/v1/auth/forgot-password // ============================================ // Request: { "email": "user@company.com" } // Response: 200 { "message": "If the email exists, a reset link has been sent" } // ============================================ // POST /api/v1/auth/reset-password // ============================================ // Request: { "token": "reset-token", "password": "NewPass123!" } // Response: 200 { "message": "Password updated successfully" } // ============================================ // POST /api/v1/auth/verify-email // ============================================ // Request: { "token": "verification-token" } // Response: 200 { "message": "Email verified" } // ============================================ // POST /api/v1/auth/2fa/enable // ============================================ // Response: { "secret": "JBSWY3DPEHPK3PXP", "qr_url": "otpauth://..." } // ============================================ // POST /api/v1/auth/2fa/verify // ============================================ // Request: { "code": "123456" } // Response: { "backup_codes": ["xxxx", "yyyy", ...] } // ============================================ // POST /api/v1/auth/sso/callback // ============================================ // Recibe callback de proveedor SSO (Google, SAML, etc.) // Request: { "provider": "google", "code": "auth_code" } // Response: { "user", "workspaces", "access_token", "refresh_token" } // ============================================ // GET /api/v1/auth/sessions // ============================================ // Lista sesiones activas del usuario // Response: [{ "id", "device_type", "device_name", "ip", "location", "last_active", "is_current" }] // ============================================ // DELETE /api/v1/auth/sessions/:id // ============================================ // Revoca una sesiΓ³n especΓ­fica // Response: 204 πŸ”Ή B. USERS MODULE // ============================================ // GET /api/v1/users // ============================================ // Lista usuarios del workspace (con paginaciΓ³n) // Query: ?search=juan&role=member&page=1&limit=50 // Response: { "users": [ { "id": "uuid", "display_name": "John Doe", "real_name": "John Doe", "email": "john@company.com", "title": "Software Engineer", "avatar_url": "https://...", "status_text": "In a meeting", "status_emoji": ":spiral_calendar:", "presence": "away", "is_bot": false, "role": "member", "timezone": "America/Bogota" } ], "total": 150, "page": 1, "limit": 50 } // ============================================ // GET /api/v1/users/:id // ============================================ // Detalle de un usuario // Response: { ...user, "profile": { "phone", "bio", "pronouns" }, "joined_at" } // ============================================ // PATCH /api/v1/users/me // ============================================ // Actualizar propio perfil // Request: { "display_name": "New Name", "title": "New Title", "avatar": "file_id" } // Response: { ...updatedUser } // ============================================ // PUT /api/v1/users/me/status // ============================================ // Actualizar status custom // Request: { "text": "On vacation", "emoji": ":palm_tree:", "expires_at": "2025-01-20T00:00:00Z" } // Response: { "status_text": "On vacation", "status_emoji": ":palm_tree:", "status_expires": "..." } // ============================================ // PUT /api/v1/users/me/presence // ============================================ // Actualizar presencia manual // Request: { "presence": "away" } // away, dnd, auto // Response: { "presence": "away" } // ============================================ // GET /api/v1/users/me/preferences // ============================================ // Preferencias del usuario // Response: { "sidebar_sections": [...], "theme": {...}, "notifications": {...} } // ============================================ // PATCH /api/v1/users/me/preferences // ============================================ // Actualizar preferencias // Request: { "theme": { "colony": "#1264a3" } } // ============================================ // GET /api/v1/users/:id/conversations // ============================================ // DMs y canales compartidos con este usuario πŸ”Ή C. WORKSPACES MODULE // ============================================ // POST /api/v1/workspaces // ============================================ // Crear nuevo workspace // Request: { "name": "My Company", "domain": "my-company" } // Response: { "id": "uuid", "name": "...", "domain": "..." } // ============================================ // GET /api/v1/workspaces/:id // ============================================ // Detalle del workspace // Response: { "id", "name", "domain", "logo_url", "plan", "member_count", "settings" } // ============================================ // PATCH /api/v1/workspaces/:id // ============================================ // Actualizar workspace (admin) // Request: { "name": "Updated Name", "logo_url": "..." } // ============================================ // GET /api/v1/workspaces/:id/members // ============================================ // Lista miembros del workspace // Query: ?role=member&page=1&limit=100 // Response: { "members": [...], "total": 150 } // ============================================ // POST /api/v1/workspaces/:id/invitations // ============================================ // Invitar usuarios // Request: { "emails": ["user1@test.com", "user2@test.com"], "role": "member" } // Response: { "invitations": [{ "email", "token", "expires_at" }] } // ============================================ // POST /api/v1/workspaces/:id/invitations/accept // ============================================ // Aceptar invitaciΓ³n // Request: { "token": "invitation-token" } // ============================================ // DELETE /api/v1/workspaces/:id/members/:userId // ============================================ // Remover usuario del workspace (admin) // Response: 204 // ============================================ // PATCH /api/v1/workspaces/:id/members/:userId // ============================================ // Cambiar rol de usuario // Request: { "role": "admin" } // ============================================ // GET /api/v1/workspaces/:id/user-groups // ============================================ // Lista grupos de usuarios // Response: [{ "id", "name", "handle", "description", "members_count" }] // ============================================ // POST /api/v1/workspaces/:id/user-groups // ============================================ // Crear grupo de usuarios // Request: { "name": "Marketing Team", "handle": "marketing", "user_ids": ["uuid1", "uuid2"] } // ============================================ // GET /api/v1/workspaces/:id/emojis // ============================================ // Lista custom emojis // Response: [{ "id", "name", "image_url", "aliases" }] // ============================================ // POST /api/v1/workspaces/:id/emojis // ============================================ // Subir custom emoji // Request: multipart/form-data { "name": "parrot", "image": File } // ============================================ // GET /api/v1/workspaces/:id/analytics // ============================================ // Analytics del workspace (admin) // Query: ?period=30d // Response: { "dau": [...], "messages_sent": 50000, "active_channels": 45, ... } πŸ”Ή D. CHANNELS MODULE (EL MÁS IMPORTANTE) // ============================================ // GET /api/v1/channels // ============================================ // Lista canales del usuario en el workspace // Query: ?type=public&archived=false&search=marketing&page=1&limit=100 // Response: { "channels": [ { "id": "uuid", "name": "marketing-team", "type": "public", "topic": "Marketing discussions", "purpose": "For all marketing team communication", "is_archived": false, "member_count": 25, "unread_count": 5, "unread_mention_count": 2, "last_read_ts": "2025-01-15T10:30:00Z", "last_message": { "text": "Let's discuss the campaign", "user": "uuid", "ts": "2025-01-15T11:00:00Z" }, "is_muted": false, "notification_pref": "all" } ], "total": 45 } // ============================================ // POST /api/v1/channels // ============================================ // Crear canal // Request: { "name": "new-project", "type": "public", // public, private "topic": "New project discussion", "purpose": "Coordinate the new product launch", "user_ids": ["uuid1", "uuid2"] // Invitar usuarios } // Response: { "id": "uuid", "name": "new-project", ... } // ============================================ // GET /api/v1/channels/:id // ============================================ // Detalle de un canal // Response: { ...channel, "members": [...], "pins": [...], "bookmarks": [...] } // ============================================ // PATCH /api/v1/channels/:id // ============================================ // Actualizar canal // Request: { "name": "renamed-channel", "topic": "New topic" } // ============================================ // POST /api/v1/channels/:id/archive // ============================================ // Archivar canal // Response: { "is_archived": true, "archived_at": "..." } // ============================================ // POST /api/v1/channels/:id/unarchive // ============================================ // Desarchivar canal // ============================================ // POST /api/v1/channels/:id/join // ============================================ // Unirse a canal pΓΊblico // Response: 204 // ============================================ // POST /api/v1/channels/:id/leave // ============================================ // Salir de canal // Response: 204 // ============================================ // POST /api/v1/channels/:id/invite // ============================================ // Invitar usuarios a canal // Request: { "user_ids": ["uuid1", "uuid2"] } // Response: { "invited": ["uuid1", "uuid2"] } // ============================================ // DELETE /api/v1/channels/:id/members/:userId // ============================================ // Remover usuario del canal (admin) // ============================================ // GET /api/v1/channels/:id/members // ============================================ // Lista miembros del canal // Query: ?page=1&limit=100 // Response: { "members": [{ "id", "display_name", "avatar_url", "role", "joined_at" }] } // ============================================ // PATCH /api/v1/channels/:id/members/me // ============================================ // Actualizar preferencias del canal para el usuario actual // Request: { "notification_pref": "mentions", "muted": true } // ============================================ // POST /api/v1/channels/:id/mark-read // ============================================ // Marcar canal como leΓ­do // Request: { "ts": "2025-01-15T11:00:00Z" } // Hasta quΓ© timestamp marcar como leΓ­do // Response: 204 // ============================================ // GET /api/v1/channels/:id/bookmarks // ============================================ // Lista bookmarks del canal // Response: [{ "id", "type", "title", "url", "emoji", "order_index" }] // ============================================ // POST /api/v1/channels/:id/bookmarks // ============================================ // Agregar bookmark // Request: { "type": "link", "title": "Docs", "url": "https://...", "emoji": ":books:" } // ============================================ // GET /api/v1/channels/:id/pins // ============================================ // Lista mensajes fijados // Response: [{ "message_id", "pinned_by", "pinned_at", "message": {...} }] // ============================================ // POST /api/v1/channels/:id/pins // ============================================ // Fijar mensaje // Request: { "message_id": "uuid" } // ============================================ // POST /api/v1/channels/:id/convert // ============================================ // Convertir canal (public ↔ private) // Request: { "to_type": "private" } // ============================================ // POST /api/v1/channels/:id/share // ============================================ // Compartir canal con otro workspace (Slack Connect) // Request: { "target_workspace_domain": "partner-company" } // Response: { "shared_channel_id": "uuid", "status": "pending" } πŸ”Ή E. MESSAGES MODULE (EL CORE) // ============================================ // GET /api/v1/channels/:id/messages // ============================================ // Lista mensajes de un canal (paginaciΓ³n por cursor) // Query: ?limit=50&cursor=2025-01-15T10:00:00Z&direction=older // Response: { "messages": [ { "id": "uuid", "user_id": "uuid", "display_name": "John Doe", "avatar_url": "https://...", "text": "Hello team!", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "Hello *team*!" } } ], "attachments": [], "mentioned_users": ["uuid2"], "reactions": [ { "emoji": ":thumbsup:", "count": 3, "users": ["uuid1", "uuid2", "uuid3"] } ], "thread_id": null, "thread_root_id": null, "reply_count": 0, "is_edited": false, "is_pinned": false, "files": [ { "id": "uuid", "filename": "doc.pdf", "url": "...", "thumbnail_url": "..." } ], "link_previews": [ { "url": "https://example.com", "title": "Example", "image": "..." } ], "ts": "2025-01-15T10:30:00Z", "created_at": "2025-01-15T10:30:00Z" } ], "has_more": true, "next_cursor": "2025-01-15T09:00:00Z" } // ============================================ // POST /api/v1/channels/:id/messages // ============================================ // Enviar mensaje // Request: { "text": "Check this out!", // Texto plano (fallback) "blocks": [ // Contenido rico (Block Kit) { "type": "section", "text": { "type": "mrkdwn", "text": "Check *this* out! :tada:" } }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "Approve" }, "action_id": "approve", "value": "yes" } ] } ], "thread_root_id": "uuid-of-parent-message", // Opcional: si es reply "file_ids": ["uuid1"], // Archivos adjuntos "scheduled_for": null, // O timestamp para programar "unfurl_links": true, // Auto preview de URLs "unfurl_media": true } // Response 201: { "id": "uuid", "channel_id": "uuid", "user_id": "uuid", "text": "Check this out!", "blocks": [...], "ts": "2025-01-15T10:31:00Z" } // ============================================ // PATCH /api/v1/messages/:id // ============================================ // Editar mensaje // Request: { "text": "Updated text", "blocks": [...] } // Response: { ...updatedMessage, "is_edited": true, "edited_at": "..." } // ============================================ // DELETE /api/v1/messages/:id // ============================================ // Borrar mensaje (soft delete) // Response: 204 // ============================================ // GET /api/v1/messages/:id/thread // ============================================ // Lista replies de un thread // Query: ?limit=50&cursor=... // Response: { "messages": [...], "has_more": true } // ============================================ // POST /api/v1/messages/:id/reactions // ============================================ // Agregar reacciΓ³n // Request: { "emoji": ":thumbsup:" } // Response: { "emoji": ":thumbsup:", "user_id": "uuid", "created_at": "..." } // ============================================ // DELETE /api/v1/messages/:id/reactions/:emoji // ============================================ // Quitar reacciΓ³n // Response: 204 // ============================================ // POST /api/v1/messages/:id/schedule // ============================================ // Programar mensaje // Request: { "text": "...", "scheduled_for": "2025-01-20T09:00:00Z" } // ============================================ // GET /api/v1/messages/scheduled // ============================================ // Lista mensajes programados del usuario // Response: [{ "id", "channel_id", "text", "scheduled_for", "status" }] // ============================================ // DELETE /api/v1/messages/scheduled/:id // ============================================ // Cancelar mensaje programado // ============================================ // POST /api/v1/messages/:id/save // ============================================ // Guardar mensaje para despuΓ©s // Response: 201 // ============================================ // DELETE /api/v1/messages/:id/save // ============================================ // Quitar de guardados // ============================================ // GET /api/v1/users/me/saved // ============================================ // Lista mensajes guardados // Response: [{ "message": {...}, "saved_at": "..." }] πŸ”Ή F. DMs MODULE // ============================================ // GET /api/v1/dms // ============================================ // Lista conversaciones DM // Response: [{ "id", "type": "dm|group_dm", "name", "members": [...], "last_message": {...}, "unread_count": 3 }] // ============================================ // POST /api/v1/dms // ============================================ // Iniciar DM o group DM // Request: { "user_ids": ["uuid1"], "initial_message": "Hi!" } // β†’ Si 1 user: DM. Si 2-9: Group DM // Response: { "channel_id": "uuid", "type": "dm" } // ============================================ // PATCH /api/v1/dms/:id // ============================================ // Renombrar group DM // Request: { "name": "Project Chat" } πŸ”Ή G. FILES MODULE // ============================================ // POST /api/v1/files/upload // ============================================ // Subir archivo (multipart) // Request: multipart/form-data // file: File (binary) // channel_id: "uuid" (opcional, compartir al subir) // title: "Document title" // comment: "Initial comment" // Response: { "id": "uuid", "filename": "document.pdf", "file_type": "pdf", "mime_type": "application/pdf", "size_bytes": 1024000, "url": "https://s3.../presigned-url", "thumbnail_url": "https://s3.../thumb-url", "upload_status": "complete" } // ============================================ // GET /api/v1/files/:id // ============================================ // Detalle de archivo // Response: { ...file, "shares": [{ "channel_id", "message_id" }], "comments": [...] } // ============================================ // GET /api/v1/files/:id/download // ============================================ // Descargar archivo (redirect a presigned URL) // Response: 302 β†’ S3 presigned URL // ============================================ // DELETE /api/v1/files/:id // ============================================ // Borrar archivo // Response: 204 // ============================================ // POST /api/v1/files/:id/share // ============================================ // Compartir archivo en canal(es) // Request: { "channel_ids": ["uuid1", "uuid2"], "comment": "Check this file" } // ============================================ // POST /api/v1/files/:id/comments // ============================================ // Comentar archivo // Request: { "text": "Great document!" } πŸ”Ή H. SEARCH MODULE // ============================================ // GET /api/v1/search // ============================================ // BΓΊsqueda global // Query: ?q=project+launch&type=all&channel_id=uuid&user_id=uuid&from=2025-01-01&to=2025-01-31&page=1&limit=20 // Response: { "query": "project launch", "results": { "messages": [ { "message": { "id", "text", "channel_id", "channel_name", "user_id", "display_name", "ts" }, "score": 0.95, "highlights": ["project launch is scheduled for..."] } ], "files": [{ "id", "filename", "file_type", "url" }], "channels": [{ "id", "name", "topic", "member_count" }], "people": [{ "id", "display_name", "title", "avatar_url" }] }, "total": 150, "took_ms": 45 } // ============================================ // POST /api/v1/search/ai // ============================================ // BΓΊsqueda con IA (pregunta en lenguaje natural) // Request: { "question": "What's the status of the marketing campaign?" } // Response: { "answer": "The marketing campaign is currently in the review phase. John mentioned on Jan 15 that the designs are ready for approval...", "sources": [ { "message_id": "uuid", "channel_name": "marketing-team", "user": "John Doe", "ts": "2025-01-15T10:00:00Z", "snippet": "The designs are ready..." } ], "confidence": 0.89 } πŸ”Ή I. LISTS MODULE // ============================================ // GET /api/v1/channels/:id/lists // ============================================ // Listas de un canal // Response: [{ "id", "name", "view_type", "description", "items_count" }] // ============================================ // POST /api/v1/channels/:id/lists // ============================================ // Crear lista // Request: { "name": "Sprint Tasks", "view_type": "table" } // ============================================ // GET /api/v1/lists/:id // ============================================ // Detalle de lista con campos // Response: { ...list, "fields": [{ "id", "name", "field_type", "options" }] } // ============================================ // POST /api/v1/lists/:id/fields // ============================================ // Agregar campo a lista // Request: { "name": "Status", "field_type": "select", "options": ["Todo", "In Progress", "Done"] } // ============================================ // GET /api/v1/lists/:id/items // ============================================ // Items de la lista // Query: ?view=board&group_by=Status&filter={"Status":"Done"}&page=1 // Response: { "items": [{ "id", "name", "field_values": {...}, "order_index" }] } // ============================================ // POST /api/v1/lists/:id/items // ============================================ // Crear item // Request: { "name": "Design landing page", "field_values": { "Assignee": "uuid", "Due Date": "2025-01-20", "Status": "Todo" } } // ============================================ // PATCH /api/v1/lists/:id/items/:itemId // ============================================ // Actualizar item // Request: { "field_values": { "Status": "Done" } } // ============================================ // DELETE /api/v1/lists/:id/items/:itemId // ============================================ // Borrar item // ============================================ // POST /api/v1/lists/:id/items/:itemId/comments // ============================================ // Comentar item // Request: { "text": "This needs revision" } // ============================================ // POST /api/v1/lists/:id/views // ============================================ // Guardar vista custom // Request: { "name": "My Tasks", "filters": {...}, "sort": {...}, "group_by": "Status", "layout": "board" } πŸ”Ή J. CANVASES MODULE // ============================================ // POST /api/v1/canvases // ============================================ // Crear canvas // Request: { "title": "Project Brief", "channel_id": "uuid", "content": {} } // Response: { "id": "uuid", "title": "...", "document_id": "uuid" } // ============================================ // GET /api/v1/canvases/:id // ============================================ // Obtener canvas // Response: { "id", "title", "content": { TipTap JSON }, "version": 3, "last_edited_by": "uuid" } // ============================================ // PATCH /api/v1/canvases/:id // ============================================ // Actualizar canvas (collaborative editing via Yjs/Hocuspocus) // Request: { "content": { updated TipTap JSON }, "version": 4 } // ============================================ // POST /api/v1/canvases/:id/collaborators // ============================================ // Agregar colaborador // Request: { "user_id": "uuid", "permission": "edit" } // ============================================ // GET /api/v1/templates // ============================================ // Lista templates // Query: ?category=onboarding // Response: [{ "id", "name", "description", "category", "type" }] // ============================================ // POST /api/v1/templates/:id/instantiate // ============================================ // Crear desde template // Request: { "channel_id": "uuid" } πŸ”Ή K. WORKFLOWS MODULE // ============================================ // GET /api/v1/workflows // ============================================ // Lista workflows // Response: [{ "id", "name", "status", "trigger_type", "execution_count" }] // ============================================ // POST /api/v1/workflows // ============================================ // Crear workflow // Request: { "name": "Welcome new members", "description": "Send welcome message when someone joins", "trigger_type": "channel_join", "trigger_config": { "channel_id": "uuid" }, "steps": [ { "type": "send_message", "config": { "channel_id": "${trigger.channel_id}", "text": "Welcome ${trigger.user_name}! πŸ‘‹" } }, { "type": "add_to_channel", "config": { "user_id": "${trigger.user_id}", "channel_id": "uuid-introductions" } } ] } // ============================================ // PATCH /api/v1/workflows/:id // ============================================ // Actualizar workflow // ============================================ // POST /api/v1/workflows/:id/activate // ============================================ // Activar workflow // ============================================ // POST /api/v1/workflows/:id/pause // ============================================ // Pausar workflow // ============================================ // GET /api/v1/workflows/:id/executions // ============================================ // Historial de ejecuciones // Response: [{ "id", "status", "started_at", "completed_at", "error_message" }] // ============================================ // POST /api/v1/workflows/generate // ============================================ // Generar workflow con IA // Request: { "prompt": "Create a workflow that sends a daily standup reminder at 9am" } // Response: { "workflow": { ...generatedWorkflow } } πŸ”Ή L. INTEGRATIONS MODULE // ============================================ // GET /api/v1/integrations/apps // ============================================ // App directory // Query: ?category=productivity&search=github // Response: [{ "slug", "name", "description", "icon_url", "categories", "installed": false }] // ============================================ // POST /api/v1/integrations/apps/:slug/install // ============================================ // Instalar app // Request: { "scopes": ["channels:read", "chat:write"] } // Response: { "oauth_url": "https://..." } β†’ Redirect usuario a OAuth // ============================================ // DELETE /api/v1/integrations/apps/:slug // ============================================ // Desinstalar app // ============================================ // POST /api/v1/integrations/webhooks/incoming // ============================================ // Crear incoming webhook // Request: { "channel_id": "uuid", "name": "CI/CD Notifications" } // Response: { "id": "uuid", "webhook_url": "https://api.chat.com/hooks/xxx" } // ============================================ // POST /api/v1/integrations/slash-commands // ============================================ // Crear slash command // Request: { "command": "/ticket", "url": "https://myapp.com/slack/command", "description": "Create a ticket" } // ============================================ // POST /api/v1/integrations/webhooks/event-subscriptions // ============================================ // Suscribirse a eventos // Request: { "callback_url": "https://myapp.com/events", "event_types": ["message.channels", "team_join"] } // ============================================ // POST /api/v1/integrations/interactive // ============================================ // Endpoint para recibir interacciones de Block Kit (botones, menus, modals) // Request: { "type": "block_actions", "actions": [{ "action_id": "approve", "value": "yes" }], "user": {...}, "channel": {...}, "message": {...} } // Response: Puede devolver un modal update o un mensaje πŸ”Ή M. AI MODULE // ============================================ // POST /api/v1/ai/chat // ============================================ // Chat con Slackbot // Request: { "message": "What did the team decide about the launch date?" } // Response: { "answer": "The team decided to launch on February 15th. This was discussed in the #product-launch channel on January 10th...", "sources": [{ "message_id", "channel_name", "snippet" }], "conversation_id": "uuid" } // ============================================ // POST /api/v1/ai/summarize/channel/:channelId // ============================================ // Resumir conversaciΓ³n de canal // Request: { "period": "24h" } // 24h, 7d, 30d, or custom range // Response: { "summary": "The team discussed three main topics: 1) The Q1 roadmap was finalized...", "key_points": ["Q1 roadmap finalized", "Budget approved for $50k", "New hire starting Feb 1"], "action_items": [{ "item": "Send budget proposal to CFO", "assignee": "John Doe" }], "participants": ["John Doe", "Jane Smith", "Bob Wilson"] } // ============================================ // POST /api/v1/ai/summarize/thread/:messageId // ============================================ // Resumir thread // Response: { "summary": "...", "key_points": [...], "decisions": [...] } // ============================================ // POST /api/v1/ai/summarize/huddle/:huddleId // ============================================ // Notas de huddle (transcripciΓ³n + resumen) // Response: { "transcript": "...", "summary": "...", "action_items": [...] } // ============================================ // POST /api/v1/ai/recaps/setup // ============================================ // Configurar recaps diarios // Request: { "channel_ids": ["uuid1", "uuid2"], "time": "09:00", "timezone": "America/Bogota" } // ============================================ // GET /api/v1/ai/recaps // ============================================ // Obtener recap de hoy // Response: { "date": "2025-01-15", "channels": [{ "channel_id", "channel_name", "summary": "..." }] } // ============================================ // POST /api/v1/ai/translate // ============================================ // Traducir mensaje // Request: { "message_id": "uuid", "target_language": "es" } // Response: { "translated_text": "Hola equipo", "detected_language": "en" } // ============================================ // POST /api/v1/ai/file-summary/:fileId // ============================================ // Resumir documento // Response: { "summary": "...", "key_points": [...], "document_type": "pdf" } πŸ”Ή N. NOTIFICATIONS MODULE // ============================================ // GET /api/v1/notifications // ============================================ // Lista notificaciones recientes // Response: [{ "id", "type", "message_id", "channel_id", "is_read", "created_at" }] // ============================================ // POST /api/v1/notifications/register-push // ============================================ // Registrar token de push notification // Request: { "platform": "ios", "token": "device-token", "device_id": "..." } // ============================================ // DELETE /api/v1/notifications/push/:tokenId // ============================================ // Desregistrar token // ============================================ // PUT /api/v1/notifications/preferences // ============================================ // Actualizar preferencias globales // Request: { "push_enabled": true, "email_frequency": "daily", "dnd_enabled": true, "dnd_start": "22:00", "dnd_end": "08:00", "keywords": ["urgent", "deadline"] } // ============================================ // PUT /api/v1/notifications/preferences/channels/:channelId // ============================================ // Preferencias por canal // Request: { "notification_pref": "mentions", "muted": true } // ============================================ // POST /api/v1/notifications/mark-all-read // ============================================ // Marcar todas como leΓ­das πŸ”Ή O. HUDDLES MODULE // ============================================ // POST /api/v1/channels/:id/huddles // ============================================ // Iniciar huddle // Request: { "is_video": false } // Response: { "id": "uuid", "room_id": "mediasoup-room-id", "ws_signaling_url": "..." } // ============================================ // POST /api/v1/huddles/:id/join // ============================================ // Unirse a huddle // Response: { "room_id", "participant_id", "ice_servers": [...] } // ============================================ // POST /api/v1/huddles/:id/leave // ============================================ // Salir de huddle // ============================================ // POST /api/v1/huddles/:id/end // ============================================ // Terminar huddle // ============================================ // GET /api/v1/huddles/:id/participants // ============================================ // Lista participantes // Response: [{ "user_id", "display_name", "avatar_url", "has_audio", "has_video", "is_screensharing" }] // ============================================ // POST /api/v1/huddles/:id/notes // ============================================ // Generar notas de huddle con IA // Response: { "transcript", "summary", "action_items": [...] } // ============================================ // POST /api/v1/huddles/:id/screen-share // ============================================ // Iniciar screen sharing πŸ”Ή P. ADMIN MODULE // ============================================ // GET /api/v1/admin/dashboard // ============================================ // Dashboard analytics // Query: ?period=30d // Response: { "dau_trend": [...], "messages_sent": 50000, "active_channels": 45, "storage_used": "120GB" } // ============================================ // GET /api/v1/admin/audit-logs // ============================================ // Logs de auditorΓ­a // Query: ?action=user.invite&actor_id=uuid&from=2025-01-01&page=1 // Response: { "logs": [{ "id", "action", "actor", "entity", "details", "created_at" }] } // ============================================ // POST /api/v1/admin/data-export // ============================================ // Exportar datos del workspace // Request: { "type": "all|messages|files|channels", "date_range": { "from": "...", "to": "..." } } // Response: { "export_id": "uuid", "status": "processing" } // ============================================ // GET /api/v1/admin/retention-policies // ============================================ // PolΓ­ticas de retenciΓ³n // ============================================ // POST /api/v1/admin/retention-policies // ============================================ // Crear polΓ­tica // Request: { "scope": "workspace", "message_retention_days": 365 } // ============================================ // POST /api/v1/admin/legal-holds // ============================================ // Crear legal hold // Request: { "name": "Litigation 2025", "user_ids": ["uuid"], "channel_ids": ["uuid"] } 4.4 WEBSOCKET SERVER (REAL-TIME GATEWAY) πŸ”Ή ImplementaciΓ³n con Socket.io // apps/realtime/src/gateways/chat.gateway.ts @WebSocketGateway({ cors: { origin: '*' }, namespace: '/', transports: ['websocket'], pingInterval: 25000, pingTimeout: 10000, }) export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; constructor( private connectionService: ConnectionService, private pubsubService: PubSubService, private presenceService: PresenceService, private authService: AuthService, ) {} // ================== CONEXIΓ“N ================== async handleConnection(client: Socket, ...args: any[]) { const token = client.handshake.auth?.token; const workspaceId = client.handshake.auth?.workspaceId; // 1. Autenticar let userId: string; try { const payload = await this.authService.verifyToken(token); userId = payload.userId; } catch { client.emit('error', { message: 'Authentication failed' }); client.disconnect(); return; } // 2. Registrar conexiΓ³n await this.connectionService.addConnection(userId, workspaceId, client.id); // 3. Unir a rooms de todos los canales del usuario const channels = await this.connectionService.getUserChannels(userId, workspaceId); channels.forEach(channelId => { client.join(`channel:${channelId}`); }); // Unir a room personal del usuario client.join(`user:${userId}`); client.join(`workspace:${workspaceId}`); // 4. Actualizar presencia a online await this.presenceService.setUserOnline(userId, workspaceId); // 5. Notificar presencia a otros await this.presenceService.broadcastPresence(userId, workspaceId, 'online'); client.userId = userId; client.workspaceId = workspaceId; console.log(`βœ… User ${userId} connected via ${client.id}`); } async handleDisconnect(client: Socket) { const { userId, workspaceId } = client; if (!userId) return; // 1. Remover conexiΓ³n await this.connectionService.removeConnection(userId, client.id); // 2. Verificar si el usuario sigue conectado en otro dispositivo/server const stillConnected = await this.connectionService.isUserConnected(userId); if (!stillConnected) { // 3. Marcar como offline await this.presenceService.setUserOffline(userId, workspaceId); await this.presenceService.broadcastPresence(userId, workspaceId, 'offline'); } console.log(`❌ User ${userId} disconnected from ${client.id}`); } // ================== EVENTOS DEL CLIENTE ================== @SubscribeMessage('subscribe_channel') async handleSubscribeChannel( @ConnectedSocket() client: Socket, @MessageBody() data: { channelId: string } ) { client.join(`channel:${data.channelId}`); await this.connectionService.subscribeToChannel(client.userId, data.channelId); client.emit('subscribed', { channelId: data.channelId }); } @SubscribeMessage('unsubscribe_channel') async handleUnsubscribeChannel( @ConnectedSocket() client: Socket, @MessageBody() data: { channelId: string } ) { client.leave(`channel:${data.channelId}`); await this.connectionService.unsubscribeFromChannel(client.userId, data.channelId); } @SubscribeMessage('typing') async handleTyping( @ConnectedSocket() client: Socket, @MessageBody() data: { channelId: string; isTyping: boolean } ) { if (data.isTyping) { // Guardar en Redis con TTL 5s await this.presenceService.setTyping(client.userId, data.channelId); } // Broadcast a otros en el canal client.to(`channel:${data.channelId}`).emit('user_typing', { userId: client.userId, channelId: data.channelId, isTyping: data.isTyping, }); } @SubscribeMessage('presence_request') async handlePresenceRequest( @ConnectedSocket() client: Socket, @MessageBody() data: { userIds: string[] } ) { const presences = await this.presenceService.getPresences(data.userIds); client.emit('presence_update', presences); } @SubscribeMessage('heartbeat') async handleHeartbeat(@ConnectedSocket() client: Socket) { await this.presenceService.heartbeat(client.userId); client.emit('heartbeat_ack'); } @SubscribeMessage('mark_read') async handleMarkRead( @ConnectedSocket() client: Socket, @MessageBody() data: { channelId: string; ts: string } ) { await this.connectionService.markChannelRead(client.userId, data.channelId, data.ts); client.emit('read_ack', { channelId: data.channelId, ts: data.ts }); } // ================== EVENTOS DEL SERVIDOR (vΓ­a Redis Pub/Sub) ================== // Estos mΓ©todos son llamados cuando se recibe un mensaje de Redis Pub/Sub // desde el API server cuando un mensaje es creado async onMessageCreated(payload: MessageCreatedEvent) { // Broadcast a todos los clientes en el canal this.server.to(`channel:${payload.channelId}`).emit('message', { id: payload.messageId, channel_id: payload.channelId, user_id: payload.userId, text: payload.text, blocks: payload.blocks, ts: payload.ts, }); } async onMessageEdited(payload: MessageEditedEvent) { this.server.to(`channel:${payload.channelId}`).emit('message_changed', { id: payload.messageId, text: payload.text, blocks: payload.blocks, edited_at: payload.editedAt, }); } async onMessageDeleted(payload: MessageDeletedEvent) { this.server.to(`channel:${payload.channelId}`).emit('message_deleted', { id: payload.messageId, channel_id: payload.channelId, deleted_by: payload.deletedBy, }); } async onReactionAdded(payload: ReactionAddedEvent) { this.server.to(`channel:${payload.channelId}`).emit('reaction_added', { message_id: payload.messageId, emoji: payload.emoji, user_id: payload.userId, }); } async onReactionRemoved(payload: ReactionRemovedEvent) { this.server.to(`channel:${payload.channelId}`).emit('reaction_removed', { message_id: payload.messageId, emoji: payload.emoji, user_id: payload.userId, }); } async onChannelJoined(payload: ChannelJoinedEvent) { this.server.to(`channel:${payload.channelId}`).emit('channel_join', { user_id: payload.userId, channel_id: payload.channelId, }); } async onChannelLeft(payload: ChannelLeftEvent) { this.server.to(`channel:${payload.channelId}`).emit('channel_leave', { user_id: payload.userId, channel_id: payload.channelId, }); } async onPresenceChange(payload: PresenceChangeEvent) { // Notificar a usuarios que tienen este usuario en su vista this.server.to(`user:${payload.userId}`).emit('presence_change', { user_id: payload.userId, presence: payload.presence, }); } async onChannelCreated(payload: ChannelCreatedEvent) { this.server.to(`workspace:${payload.workspaceId}`).emit('channel_created', { id: payload.channelId, name: payload.name, type: payload.type, }); } async onHuddleStarted(payload: HuddleStartedEvent) { this.server.to(`channel:${payload.channelId}`).emit('huddle_start', { huddle_id: payload.huddleId, channel_id: payload.channelId, started_by: payload.userId, }); } async onHuddleEnded(payload: HuddleEndedEvent) { this.server.to(`channel:${payload.channelId}`).emit('huddle_end', { huddle_id: payload.huddleId, }); } async onListItemUpdated(payload: ListItemUpdatedEvent) { this.server.to(`channel:${payload.channelId}`).emit('list_item_updated', { list_id: payload.listId, item_id: payload.itemId, changes: payload.changes, }); } } πŸ”Ή Redis Pub/Sub Service (Cross-Server Broadcasting) // apps/realtime/src/services/pubsub.service.ts @Injectable() export class PubSubService { private pubClient: Redis; private subClient: Redis; constructor(private eventEmitter: EventEmitter2) { this.pubClient = new Redis(process.env.REDIS_URL); this.subClient = new Redis(process.env.REDIS_URL); this.setupSubscriptions(); } private setupSubscriptions() { // Suscribirse a todos los canales de eventos this.subClient.subscribe('events:message_created'); this.subClient.subscribe('events:message_edited'); this.subClient.subscribe('events:message_deleted'); this.subClient.subscribe('events:reaction_added'); this.subClient.subscribe('events:reaction_removed'); this.subClient.subscribe('events:channel_joined'); this.subClient.subscribe('events:channel_left'); this.subClient.subscribe('events:channel_created'); this.subClient.subscribe('events:presence_change'); this.subClient.subscribe('events:huddle_started'); this.subClient.subscribe('events:huddle_ended'); this.subClient.subscribe('events:list_item_updated'); this.subClient.subscribe('events:notification'); this.subClient.on('message', (channel, message) => { const payload = JSON.parse(message); // Emitir internamente para que el ChatGateway lo maneje this.eventEmitter.emit(channel, payload); }); } async publish(event: string, payload: any) { await this.pubClient.publish(`events:${event}`, JSON.stringify(payload)); } } πŸ”Ή Connection Service (GestiΓ³n de conexiones) // apps/realtime/src/services/connection.service.ts @Injectable() export class ConnectionService { constructor(private redis: Redis) {} async addConnection(userId: string, workspaceId: string, socketId: string) { // Trackear conexiΓ³n por usuario await this.redis.sadd(`connections:user:${userId}`, socketId); await this.redis.sadd(`connections:workspace:${workspaceId}`, userId); await this.redis.hset(`connection:${socketId}`, { userId, workspaceId }); // Expirar en 24h por si se pierde la desconexiΓ³n await this.redis.expire(`connections:user:${userId}`, 86400); } async removeConnection(userId: string, socketId: string) { await this.redis.srem(`connections:user:${userId}`, socketId); await this.redis.del(`connection:${socketId}`); } async isUserConnected(userId: string): Promise { const count = await this.redis.scard(`connections:user:${userId}`); return count > 0; } async getUserChannels(userId: string, workspaceId: string): Promise { // Query a DB o cache para obtener canales del usuario // Esto deberΓ­a estar cacheado en Redis const channels = await this.redis.smembers(`user_channels:${userId}:${workspaceId}`); return channels; } async subscribeToChannel(userId: string, channelId: string) { await this.redis.sadd(`user_channels:${userId}:*`, channelId); } async unsubscribeFromChannel(userId: string, channelId: string) { await this.redis.srem(`user_channels:${userId}:*`, channelId); } async markChannelRead(userId: string, channelId: string, ts: string) { // Actualizar en Redis (cache) + DB (persistente) await this.redis.hset(`read_state:${userId}`, channelId, ts); // TambiΓ©n actualizar en DB de forma async } } πŸ”Ή Presence Service // apps/realtime/src/services/presence.service.ts @Injectable() export class PresenceService { constructor( private redis: Redis, private pubsubService: PubSubService, ) {} async setUserOnline(userId: string, workspaceId: string) { await this.redis.set(`presence:${userId}`, 'online', 'EX', 60); await this.redis.sadd(`presence:workspace:${workspaceId}`, userId); } async setUserOffline(userId: string, workspaceId: string) { await this.redis.del(`presence:${userId}`); await this.redis.srem(`presence:workspace:${workspaceId}`, userId); } async heartbeat(userId: string) { // Renovar TTL de presencia const current = await this.redis.get(`presence:${userId}`); if (current === 'online') { await this.redis.expire(`presence:${userId}`, 60); } } async getPresence(userId: string): Promise { const presence = await this.redis.get(`presence:${userId}`); return presence || 'offline'; } async getPresences(userIds: string[]): Promise> { const pipeline = this.redis.pipeline(); userIds.forEach(id => pipeline.get(`presence:${id}`)); const results = await pipeline.exec(); const presences: Record = {}; userIds.forEach((id, index) => { presences[id] = results[index][1] || 'offline'; }); return presences; } async setTyping(userId: string, channelId: string) { await this.redis.set(`typing:${channelId}:${userId}`, Date.now().toString(), 'EX', 5); } async getTypingUsers(channelId: string): Promise { const keys = await this.redis.keys(`typing:${channelId}:*`); const userIds = keys.map(key => key.split(':')[2]); return userIds; } async broadcastPresence(userId: string, workspaceId: string, presence: string) { await this.pubsubService.publish('presence_change', { userId, workspaceId, presence, }); } } 4.5 FLUJO COMPLETO DE UN MENSAJE (END-TO-END) πŸ”Ή Desde que el usuario presiona "Enviar" hasta que llega a todos los clientes PASO 1: CLIENTE envΓ­a mensaje β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Usuario presiona "Enviar" en el web/mobile app β”‚ β”‚ β”‚ β”‚ POST /api/v1/channels/:channelId/messages β”‚ β”‚ Authorization: Bearer {token} β”‚ β”‚ Body: { text: "Hello team!", blocks: [...] } β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό PASO 2: API GATEWAY recibe la peticiΓ³n β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β€’ JwtAuthGuard verifica el token JWT β”‚ β”‚ β€’ WorkspaceContextMiddleware verifica membresΓ­a β”‚ β”‚ β€’ RateLimitInterceptor: ΒΏUsuario envΓ­aε€ͺ倚 mensajes? β”‚ β”‚ β€’ ValidationPipe: ΒΏBody vΓ‘lido? β”‚ β”‚ β†’ Pasa al MessagesController.sendMessage() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό PASO 3: MESSAGE SERVICE procesa β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ MessagesService.createMessage() β”‚ β”‚ β”‚ β”‚ 3a. Parse mentions (@user, @channel, #channel) β”‚ β”‚ β†’ Extraer mentioned_users[], mentioned_channels[] β”‚ β”‚ β”‚ β”‚ 3b. Guardar en PostgreSQL β”‚ β”‚ INSERT INTO messages (...) VALUES (...) β”‚ β”‚ β†’ message.id = uuid, message.ts = NOW() β”‚ β”‚ β”‚ β”‚ 3c. Actualizar channel.last_message_at β”‚ β”‚ UPDATE channels SET last_message_at = NOW() β”‚ β”‚ β”‚ β”‚ 3d. Incrementar unread_count de otros miembros β”‚ β”‚ UPDATE channel_members β”‚ β”‚ SET unread_count = unread_count + 1 β”‚ β”‚ WHERE channel_id = ? AND user_id != sender β”‚ β”‚ β”‚ β”‚ 3e. Si hay mentions: incrementar unread_mention_count β”‚ β”‚ β”‚ β”‚ 3f. Publicar evento a Redis Pub/Sub β”‚ β”‚ PUBLISH events:message_created { β”‚ β”‚ messageId, channelId, userId, text, blocks, ts β”‚ β”‚ } β”‚ β”‚ β”‚ β”‚ 3g. Encolar jobs asΓ­ncronos: β”‚ β”‚ - notifications.queue.add({ message, mentions }) β”‚ β”‚ - search-indexer.queue.add({ message }) β”‚ β”‚ - embeddings.queue.add({ message }) β”‚ β”‚ - link-unfurl.queue.add({ message }) (si tiene URLs) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό PASO 4: REDIS PUB/PROPaga el evento β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Redis PUBLISH events:message_created β”‚ β”‚ β”‚ β”‚ β†’ Todos los WebSocket servers suscritos reciben el evento β”‚ β”‚ β†’ Cada WS server lo procesa en su ChatGateway.onMessageCreatedβ”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό PASO 5: WEBSOCKET SERVERS broadcast a clientes β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ChatGateway.onMessageCreated(payload) β”‚ β”‚ β”‚ β”‚ β†’ this.server.to(`channel:${channelId}`).emit('message', { β”‚ β”‚ id, channel_id, user_id, text, blocks, ts β”‚ β”‚ }) β”‚ β”‚ β”‚ β”‚ β†’ Socket.io envΓ­a el evento a TODOS los clientes β”‚ β”‚ conectados que estΓ‘n en el room `channel:${channelId}` β”‚ β”‚ en TODOS los WebSocket servers β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό PASO 6: CLIENTES reciben el mensaje β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Cada cliente (web, desktop, mobile) recibe: β”‚ β”‚ β”‚ β”‚ socket.on('message', (message) => { β”‚ β”‚ // Agregar mensaje al store (Zustand/Redux) β”‚ β”‚ // Actualizar UI: scroll al fondo, incrementar unread β”‚ β”‚ // Si tiene mention: reproducir sonido, mostrar badge β”‚ β”‚ }) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ PARALELO: Jobs asΓ­ncronos se ejecutan en background β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ JOB 1: Notifications Processor β”‚ β”‚ β†’ Determinar quiΓ©n necesita notificaciΓ³n β”‚ β”‚ β†’ Verificar preferencias de cada usuario β”‚ β”‚ β†’ Verificar DND β”‚ β”‚ β†’ Enviar push notification (FCM/APNs) si aplica β”‚ β”‚ β†’ Enviar email si aplica β”‚ β”‚ β†’ Actualizar badge count β”‚ β”‚ β”‚ β”‚ JOB 2: Search Indexer β”‚ β”‚ β†’ Indexar mensaje en Meilisearch/Elasticsearch β”‚ β”‚ β†’ { id, workspace_id, channel_id, text, user_id, ts } β”‚ β”‚ β”‚ β”‚ JOB 3: Embeddings Generator β”‚ β”‚ β†’ Generar embedding del texto (OpenAI API) β”‚ β”‚ β†’ Guardar en ai.message_embeddings β”‚ β”‚ β†’ Para futura bΓΊsqueda semΓ‘ntica con IA β”‚ β”‚ β”‚ β”‚ JOB 4: Link Unfurler β”‚ β”‚ β†’ Si el mensaje contiene URLs β”‚ β”‚ β†’ Fetch URL β†’ extraer title, description, image (OG tags) β”‚ β”‚ β†’ Actualizar message.link_previews β”‚ β”‚ β†’ Publicar events:message_edited para actualizar clientes β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ πŸ”Ή Diagrama visual del flujo: Usuario A API Server Redis Pub/Sub WS Server 1 WS Server 2 Usuario B β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ │──POST /messages───────────▢│ β”‚ β”‚ β”‚ β”‚ β”‚ │──INSERT INTO DB───▢ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ │──PUBLISH events:msg──▢│ β”‚ β”‚ β”‚ β”‚ β”‚ │──broadcast──────────▢│ β”‚ β”‚ β”‚ β”‚ │──broadcast───────────────────────────────▢│ β”‚ │◀──201 Created──────────────│ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ │──emit('message')──▢│ β”‚ β”‚ β”‚ β”‚ β”‚ │──emit('message')──▢│ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ │──enqueue notificationsβ–Ά (BullMQ) β”‚ β”‚ β”‚ β”‚ │──enqueue search indexβ–Ά (BullMQ) β”‚ β”‚ β”‚ β”‚ │──enqueue embeddings───▢ (BullMQ) β”‚ β”‚ β”‚ β”‚ │──enqueue link unfurl─▢ (BullMQ) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Usuario B ve β”‚ β”‚ β”‚ β”‚ β”‚ │──el mensaje al instante β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ NotificaciΓ³n β”‚ β”‚ β”‚ β”‚ β”‚ │◀──push (FCM/APNs)──│ 4.6 WORKER PROCESSORS (BACKGROUND JOBS) πŸ”Ή Notifications Processor // apps/workers/src/processors/notifications.processor.ts @Processor('notifications', { concurrency: 50 }) export class NotificationsProcessor extends WorkerHost { constructor( private pushService: PushNotificationService, private emailService: EmailService, private preferenceService: NotificationPreferenceService, ) { super(); } async process(job: Job) { const { message, channel, mentionedUserIds, senderId } = job.data; // 1. Determinar destinatarios const recipientIds = await this.getRecipients(channel.id, senderId, mentionedUserIds); for (const userId of recipientIds) { // 2. Verificar preferencias const prefs = await this.preferenceService.get(userId, channel.workspaceId); const shouldNotify = this.evaluateNotificationRules({ prefs, isMentioned: mentionedUserIds?.includes(userId), isDM: channel.type === 'dm', channelMuted: await this.isChannelMuted(userId, channel.id), isDND: this.isDNDActive(prefs), }); if (!shouldNotify) continue; // 3. Determinar canales de entrega const channels = this.determineDeliveryChannels(prefs, userId); // 4. Enviar notificaciones if (channels.push) { await this.pushService.send(userId, { title: `New message in #${channel.name}`, body: this.truncateMessage(message.text), data: { channelId: channel.id, messageId: message.id }, }); } if (channels.email && prefs.emailFrequency === 'immediate') { await this.emailService.sendNotification(userId, message, channel); } } } private evaluateNotificationRules(params: NotificationRules): boolean { if (params.isDND && !params.isMentioned) return false; if (params.channelMuted && !params.isMentioned) return false; if (params.isMentioned && params.prefs.notifyMentions) return true; if (params.isDM && params.prefs.notifyDMs) return true; if (params.prefs.notifyAllMessages) return true; return false; } } πŸ”Ή Search Indexer Processor // apps/workers/src/processors/search-indexer.processor.ts @Processor('search-indexing') export class SearchIndexerProcessor extends WorkerHost { constructor(private searchService: SearchService) { super(); } async process(job: Job<{ message: Message }>) { const { message } = job.data; await this.searchService.indexMessage({ id: message.id, workspace_id: message.workspaceId, channel_id: message.channelId, channel_name: message.channelName, user_id: message.userId, user_name: message.userName, text: message.text, blocks_text: this.extractTextFromBlocks(message.blocks), ts: message.ts, }); } } πŸ”Ή Scheduled Messages Processor // apps/workers/src/processors/scheduled-messages.processor.ts @Processor('scheduled-messages', { repeat: { pattern: '*/1 * * * *' }, // Cada minuto }) export class ScheduledMessagesProcessor extends WorkerHost { constructor(private messageService: MessageService) { super(); } async process() { // Buscar mensajes programados que toca enviar const dueMessages = await this.messageService.getDueScheduledMessages(); for (const scheduled of dueMessages) { try { // Enviar el mensaje const message = await this.messageService.createMessage({ channelId: scheduled.channelId, userId: scheduled.userId, text: scheduled.text, blocks: scheduled.blocks, }); // Marcar como enviado await this.messageService.markScheduledAsSent(scheduled.id, message.id); } catch (error) { console.error(`Failed to send scheduled message ${scheduled.id}:`, error); } } } } 4.7 DEFINICIΓ“N COMPLETA DE EVENTOS WEBSOCKET // apps/realtime/src/events/events.ts // ============== EVENTOS CLIENTE β†’ SERVIDOR ============== interface ClientToServerEvents { // SuscripciΓ³n 'subscribe_channel': (data: { channelId: string }) => void; 'unsubscribe_channel': (data: { channelId: string }) => void; // Presencia 'typing': (data: { channelId: string; isTyping: boolean }) => void; 'presence_request': (data: { userIds: string[] }) => void; 'heartbeat': () => void; // Estado de lectura 'mark_read': (data: { channelId: string; ts: string }) => void; // Huddles (seΓ±alizaciΓ³n WebRTC) 'huddle_signal': (data: { huddleId: string; signal: any; toUserId: string }) => void; 'huddle_join': (data: { huddleId: string }) => void; 'huddle_leave': (data: { huddleId: string }) => void; // Canvas collaborative editing (Yjs sync) 'canvas_update': (data: { canvasId: string; update: Uint8Array }) => void; } // ============== EVENTOS SERVIDOR β†’ CLIENTE ============== interface ServerToClientEvents { // Mensajes 'message': (data: MessageEvent) => void; 'message_changed': (data: MessageChangedEvent) => void; 'message_deleted': (data: MessageDeletedEvent) => void; // Reacciones 'reaction_added': (data: ReactionEvent) => void; 'reaction_removed': (data: ReactionEvent) => void; // Canales 'channel_created': (data: ChannelCreatedEvent) => void; 'channel_join': (data: ChannelJoinEvent) => void; 'channel_leave': (data: ChannelLeaveEvent) => void; 'channel_archive': (data: ChannelArchiveEvent) => void; 'channel_rename': (data: ChannelRenameEvent) => void; 'channel_topic': (data: ChannelTopicEvent) => void; // Presencia y typing 'user_typing': (data: TypingEvent) => void; 'presence_change': (data: PresenceEvent) => void; 'user_status_change': (data: StatusChangeEvent) => void; // Archivos 'file_shared': (data: FileSharedEvent) => void; // Pins y bookmarks 'pin_added': (data: PinEvent) => void; 'pin_removed': (data: PinEvent) => void; // Huddles 'huddle_start': (data: HuddleStartEvent) => void; 'huddle_end': (data: HuddleEndEvent) => void; 'huddle_signal': (data: any) => void; 'huddle_user_joined': (data: any) => void; 'huddle_user_left': (data: any) => void; // Lists 'list_item_updated': (data: any) => void; 'list_item_created': (data: any) => void; 'list_item_deleted': (data: any) => void; // Canvas 'canvas_update': (data: any) => void; // Notificaciones 'notification': (data: NotificationEvent) => void; 'badge_update': (data: { count: number }) => void; // Sistema 'error': (data: { message: string; code?: string }) => void; 'subscribed': (data: { channelId: string }) => void; 'read_ack': (data: { channelId: string; ts: string }) => void; 'heartbeat_ack': () => void; 'reconnect': () => void; } // ============== TIPOS DE EVENTOS ============== interface MessageEvent { id: string; channel_id: string; user_id: string; display_name: string; avatar_url: string; text: string; blocks: any[]; thread_root_id: string | null; mentioned_users: string[]; files: any[]; link_previews: any[]; ts: string; } interface ReactionEvent { message_id: string; channel_id: string; emoji: string; user_id: string; } 4.8 RATE LIMITING Y SEGURIDAD // apps/api/src/common/interceptors/rate-limit.interceptor.ts @Injectable() export class RateLimitInterceptor implements NestInterceptor { constructor(private redis: Redis) {} async intercept(context: ExecutionContext, next: CallHandler) { const request = context.switchToHttp().getRequest(); const userId = request.user?.userId; const endpoint = request.route?.path || request.url; // Rate limits por endpoint const limits = this.getLimits(endpoint); for (const { window, max } of limits) { const key = `ratelimit:${userId}:${endpoint}:${window}`; const current = await this.redis.incr(key); if (current === 1) { await this.redis.expire(key, window); } if (current > max) { throw new TooManyRequestsException( `Rate limit exceeded. Max ${max} requests per ${window}s` ); } } return next.handle(); } private getLimits(endpoint: string): Array<{window: number, max: number}> { // Endpoints crΓ­ticos con lΓ­mites estrictos if (endpoint.includes('/messages') && request.method === 'POST') { return [{ window: 1, max: 5 }, { window: 60, max: 100 }]; // 5/s, 100/min } if (endpoint.includes('/files/upload')) { return [{ window: 60, max: 20 }]; // 20/min } if (endpoint.includes('/ai/')) { return [{ window: 60, max: 30 }]; // 30/min } // Default return [{ window: 60, max: 300 }]; // 300/min } } 4.9 ERROR HANDLING GLOBAL // apps/api/src/common/filters/all-exceptions.filter.ts @Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); let status = 500; let message = 'Internal server error'; let code = 'INTERNAL_ERROR'; let details: any; if (exception instanceof HttpException) { status = exception.getStatus(); const response = exception.getResponse(); if (typeof response === 'object') { message = (response as any).message || message; code = (response as any).code || code; details = (response as any).details; } } else if (exception instanceof Prisma.PrismaClientKnownRequestError) { status = 400; code = `DB_ERROR_${exception.code}`; message = 'Database constraint violation'; } // Log del error console.error(`[${request.requestId}] ${status} ${code}: ${message}`, exception); // Response estandarizada response.status(status).json({ ok: false, error: { code, message, details, }, timestamp: new Date().toISOString(), path: request.url, requestId: request.requestId, }); } } 4.10 ARQUITECTURA DE RESPUESTA API ESTÁNDAR // Todas las respuestas siguen este formato: // Γ‰XITO: { "ok": true, "data": { ... }, "meta": { "page": 1, "limit": 50, "total": 150, "has_more": true } } // ERROR: { "ok": false, "error": { "code": "NOT_FOUND", "message": "Channel not found", "details": { "channel_id": "uuid" } }, "timestamp": "2025-01-15T10:30:00Z", "path": "/api/v1/channels/uuid", "requestId": "req-uuid" } πŸ“Œ PARTE 5: FRONTEND COMPLETO β€” WEB, DESKTOP, MOBILE + UX/UI 5.1 ARQUITECTURA DEL FRONTEND πŸ”Ή Estructura Monorepo (Turborepo) frontend/ β”œβ”€β”€ apps/ β”‚ β”œβ”€β”€ web/ β†’ Next.js 14 (App Router) β”‚ β”‚ β”œβ”€β”€ src/ β”‚ β”‚ β”‚ β”œβ”€β”€ app/ β†’ Rutas (App Router) β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ (auth)/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ login/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ register/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ forgot-password/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── layout.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ (workspace)/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ [workspaceDomain]/ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ layout.tsx β†’ Layout principal con sidebar β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx β†’ Redirect a #general β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ channel/[channelId]/ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx β†’ Vista de canal β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ thread/[messageId]/page.tsx β†’ Thread view β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ canvas/page.tsx β†’ Canvas del canal β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ lists/page.tsx β†’ Lists del canal β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └── settings/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ dm/[dmId]/page.tsx β†’ Vista de DM β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ huddle/[huddleId]/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ activity/page.tsx β†’ Activity feed β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ later/page.tsx β†’ Saved items β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ threads/page.tsx β†’ All threads β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └── settings/ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ notifications/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └── profile/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── layout.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ admin/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx β†’ Dashboard admin β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ members/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ channels/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ analytics/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ audit-logs/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ integrations/page.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── security/page.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ layout.tsx β†’ Root layout β”‚ β”‚ β”‚ β”‚ └── globals.css β”‚ β”‚ β”‚ β”œβ”€β”€ components/ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ layout/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Sidebar.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ SidebarSection.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ChannelList.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ DMList.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ TopBar.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ RightPanel.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── WorkspaceSwitcher.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ messaging/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ MessageList.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ MessageItem.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ MessageInput.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ MessageEditor.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ThreadView.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ MessageReactions.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ MessageActions.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── SystemMessage.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ channels/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ChannelHeader.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ChannelInfo.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ChannelMembers.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ CreateChannelModal.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ChannelBrowseModal.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── InviteMembersModal.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ block-kit/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ BlockRenderer.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ SectionBlock.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ActionsBlock.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ DividerBlock.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ImageBlock.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ContextBlock.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ HeaderBlock.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ModalRenderer.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── InteractiveElements.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ files/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ FilePreview.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ FileUploadZone.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ImagePreview.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ VideoPreview.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── FileIcon.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ huddles/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ HuddleBar.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ HuddlePanel.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ VideoTile.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ AudioIndicator.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── ScreenShareView.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ lists/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ListTable.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ListBoard.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ListItem.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── ListItemEditor.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ canvas/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ CanvasEditor.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── CanvasToolbar.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ search/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ SearchBar.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ SearchResults.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── AISearchResults.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ai/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ SlackbotPanel.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ SummaryCard.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── RecapView.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ common/ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Avatar.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ PresenceDot.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ EmojiPicker.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ UserMention.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Tooltip.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Modal.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Dropdown.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Toast.tsx β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ CommandPalette.tsx β”‚ β”‚ β”‚ β”‚ β”‚ └── InfiniteScroll.tsx β”‚ β”‚ β”‚ β”‚ └── ui/ β†’ shadcn/ui components β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ button.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ dialog.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ input.tsx β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ popover.tsx β”‚ β”‚ β”‚ β”‚ └── ... β”‚ β”‚ β”‚ β”œβ”€β”€ hooks/ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ useSocket.ts β†’ WebSocket connection β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ useChannel.ts β†’ Canal actual β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ useMessages.ts β†’ Mensajes con paginaciΓ³n β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ usePresence.ts β†’ Presencia de usuarios β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ useTyping.ts β†’ Typing indicators β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ useNotifications.ts β†’ Notificaciones β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ useHuddle.ts β†’ Huddle state β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ useMediaQuery.ts β”‚ β”‚ β”‚ β”‚ └── useKeyboardShortcuts.ts β”‚ β”‚ β”‚ β”œβ”€β”€ stores/ β†’ Zustand stores β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ workspace.store.ts β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ channels.store.ts β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ messages.store.ts β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ users.store.ts β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ presence.store.ts β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ notifications.store.ts β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ui.store.ts β†’ Modals, panels, theme β”‚ β”‚ β”‚ β”‚ └── huddle.store.ts β”‚ β”‚ β”‚ β”œβ”€β”€ lib/ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ api-client.ts β†’ Axios/fetch wrapper β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ socket-client.ts β†’ Socket.io client setup β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ block-kit-parser.ts β†’ Parser de Block Kit β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ message-formatter.ts β†’ Markdown β†’ HTML β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ mentions-parser.ts β†’ @mentions parsing β”‚ β”‚ β”‚ β”‚ └── utils.ts β”‚ β”‚ β”‚ └── types/ β”‚ β”‚ β”‚ └── index.ts β†’ Tipos compartidos β”‚ β”‚ β”œβ”€β”€ public/ β”‚ β”‚ β”œβ”€β”€ next.config.js β”‚ β”‚ β”œβ”€β”€ tailwind.config.js β”‚ β”‚ └── package.json β”‚ β”‚ β”‚ β”œβ”€β”€ desktop/ β†’ Tauri (Rust + WebView) β”‚ β”‚ β”œβ”€β”€ src-tauri/ β”‚ β”‚ β”‚ β”œβ”€β”€ src/main.rs β”‚ β”‚ β”‚ β”œβ”€β”€ Cargo.toml β”‚ β”‚ β”‚ └── tauri.conf.json β”‚ β”‚ β”œβ”€β”€ src/ β†’ Misma app web embebida β”‚ β”‚ └── package.json β”‚ β”‚ β”‚ └── mobile/ β†’ React Native (Expo) β”‚ β”œβ”€β”€ src/ β”‚ β”‚ β”œβ”€β”€ screens/ β”‚ β”‚ β”‚ β”œβ”€β”€ LoginScreen.tsx β”‚ β”‚ β”‚ β”œβ”€β”€ HomeScreen.tsx β”‚ β”‚ β”‚ β”œβ”€β”€ ChannelScreen.tsx β”‚ β”‚ β”‚ β”œβ”€β”€ DMScreen.tsx β”‚ β”‚ β”‚ β”œβ”€β”€ ThreadScreen.tsx β”‚ β”‚ β”‚ β”œβ”€β”€ HuddleScreen.tsx β”‚ β”‚ β”‚ └── SettingsScreen.tsx β”‚ β”‚ β”œβ”€β”€ components/ β”‚ β”‚ β”œβ”€β”€ navigation/ β”‚ β”‚ β”œβ”€β”€ stores/ β”‚ β”‚ β”œβ”€β”€ hooks/ β”‚ β”‚ └── lib/ β”‚ β”œβ”€β”€ app.json β”‚ └── package.json β”‚ β”œβ”€β”€ packages/ β”‚ β”œβ”€β”€ ui/ β†’ Componentes UI compartidos β”‚ β”œβ”€β”€ types/ β†’ Tipos TypeScript compartidos β”‚ β”œβ”€β”€ api/ β†’ API client compartido β”‚ └── utils/ β†’ Utils compartidos β”‚ β”œβ”€β”€ package.json └── turbo.json 5.2 LAYOUT PRINCIPAL (WEB) πŸ”Ή Estructura visual del layout β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ TOP BAR: Workspace Name β–Ό β”‚ Search Bar β”‚ Help β”‚ Notifications β”‚ Profile β”‚ Dark β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ CHANNEL HEADER: #marketing-team β”‚ RIGHT PANEL β”‚ β”‚ SIDEBAR β”‚ Topic: Marketing discussions | Members: 25 β”‚ (Tabs: Info, Files, β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Canvas, Lists, etc) β”‚ β”‚ β–Ό Channels β”‚ β”‚ β”‚ β”‚ # general β”‚ MESSAGE LIST β”‚ Channel Details β”‚ β”‚ # marketing β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ # dev-team β”‚ β”‚ πŸ‘€ John Doe 10:30 AM β”‚ β”‚ Topic: β”‚ β”‚ # random β”‚ β”‚ Hello team! Check this out πŸ‘‹ β”‚ β”‚ Marketing discus... β”‚ β”‚ # support β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + Add β”‚ β”‚ πŸ‘€ Jane Smith 10:32 AM β”‚ β”‚ Created by: β”‚ β”‚ β”‚ β”‚ Looks great! πŸ‘ β”‚ β”‚ John Doe β”‚ β”‚ β–Ό DMs β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ πŸ’¬ Bob β”‚ β”‚ πŸ‘€ John Doe 10:35 AM β”‚ β”‚ Members (25): β”‚ β”‚ πŸ’¬ Alice β”‚ β”‚ Here's the document: β”‚ β”‚ β”Œβ”€β”€β”€β” β”‚ β”‚ + Add β”‚ β”‚ πŸ“„ Q1-Marketing-Plan.pdf β”‚ β”‚ β”‚JD β”‚ John Doe β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€ β”‚ β”‚ β–Ό Apps β”‚ β”‚ πŸ’¬ 2 replies (thread) β”‚ β”‚ β”‚JS β”‚ Jane Smith β”‚ β”‚ πŸ€– GitHub β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€ β”‚ β”‚ πŸ€– Jira β”‚ β”‚ πŸ‘€ System 10:40 AM β”‚ β”‚ β”‚BW β”‚ Bob Wilson β”‚ β”‚ β”‚ β”‚ Bob Wilson joined #marketing-team β”‚ β”‚ β””β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ Pinned Messages: β”‚ β”‚ β”‚ MESSAGE INPUT β”‚ πŸ“Œ Project deadline β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ πŸ“Œ Meeting notes β”‚ β”‚ β”‚ β”‚ Write a message... πŸ“Ž 😊 @ # /sh β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Bookmarks: β”‚ β”‚ β”‚ β”‚ πŸ”— Marketing Docs β”‚ β”‚ ● Online β”‚ β”‚ πŸ”— Campaign Tracker β”‚ β”‚ John Doe β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ πŸ”Ή Root Layout // apps/web/src/app/layout.tsx import { Inter } from 'next/font/google'; import { Providers } from '@/components/Providers'; import { Toaster } from '@/components/common/Toast'; import './globals.css'; const inter = Inter({ subsets: ['latin'] }); export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } πŸ”Ή Providers (Context & State) // apps/web/src/components/Providers.tsx 'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { SocketProvider } from '@/hooks/useSocket'; import { ThemeProvider } from 'next-themes'; import { useState } from 'react'; export function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState(() => new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, // 30 segundos cacheTime: 5 * 60_000, // 5 minutos refetchOnWindowFocus: false, retry: 2, }, }, })); return ( {children} {process.env.NODE_ENV === 'development' && } ); } πŸ”Ή Workspace Layout (con Sidebar) // apps/web/src/app/(workspace)/[workspaceDomain]/layout.tsx 'use client'; import { Sidebar } from '@/components/layout/Sidebar'; import { TopBar } from '@/components/layout/TopBar'; import { useWorkspaceStore } from '@/stores/workspace.store'; import { useEffect } from 'react'; import { useSocket } from '@/hooks/useSocket'; export default function WorkspaceLayout({ children, params, }: { children: React.ReactNode; params: { workspaceDomain: string }; }) { const { loadWorkspace, currentWorkspace } = useWorkspaceStore(); const { connect, disconnect } = useSocket(); useEffect(() => { loadWorkspace(params.workspaceDomain); }, [params.workspaceDomain]); useEffect(() => { if (currentWorkspace) { connect(currentWorkspace.id); } return () => disconnect(); }, [currentWorkspace?.id]); return (
{/* Sidebar - 260px fijo, colapsable a 60px */} {/* Main area */}
{/* Contenido principal */}
{children}
{/* Right panel (condicional) */} {/* */}
); } 5.3 SIDEBAR (Componente principal de navegaciΓ³n) πŸ”Ή Sidebar Component // apps/web/src/components/layout/Sidebar.tsx 'use client'; import { WorkspaceSwitcher } from './WorkspaceSwitcher'; import { SidebarSection } from './SidebarSection'; import { ChannelList } from './ChannelList'; import { DMList } from './DMList'; import { UserPresenceCard } from '@/components/common/UserPresenceCard'; import { useChannelsStore } from '@/stores/channels.store'; import { useUsersStore } from '@/stores/users.store'; import { Search } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useState } from 'react'; export function Sidebar() { const [collapsed, setCollapsed] = useState(false); const { channels } = useChannelsStore(); const { currentUser } = useUsersStore(); const publicChannels = channels.filter(c => c.type === 'public' && !c.is_archived); const privateChannels = channels.filter(c => c.type === 'private' && !c.is_archived); const dms = channels.filter(c => c.type === 'dm' || c.type === 'group_dm'); return ( ); } πŸ”Ή ChannelList Component // apps/web/src/components/layout/ChannelList.tsx 'use client'; import { useState } from 'react'; import { ChevronDown, ChevronRight, Plus, Hash, Lock } from 'lucide-react'; import { useChannelsStore } from '@/stores/channels.store'; import { useNotificationsStore } from '@/stores/notifications.store'; import { cn } from '@/lib/utils'; interface ChannelListProps { title: string; channels: Channel[]; collapsed: boolean; defaultCollapsed?: boolean; } export function ChannelList({ title, channels, collapsed, defaultCollapsed }: ChannelListProps) { const [isOpen, setIsOpen] = useState(!defaultCollapsed); const { setActiveChannel } = useChannelsStore(); const { unreadCounts } = useNotificationsStore(); const [filter, setFilter] = useState(''); const [showFilter, setShowFilter] = useState(false); const filtered = filter ? channels.filter(c => c.name.includes(filter.toLowerCase())) : channels; if (collapsed) { return (
); } return (
{/* Section header */}
{/* Filter input */} {showFilter && ( setFilter(e.target.value)} placeholder="Filter channels..." className="mx-2 mb-1 w-[calc(100%-1rem)] rounded border border-slack-border bg-slack-input px-2 py-1 text-sm" /> )} {/* Channel items */} {isOpen && (
{filtered.map(channel => ( setActiveChannel(channel.id)} /> ))} {filtered.length === 0 && filter && (
No channels found
)}
)}
); } function ChannelItem({ channel, unread, mentions, onClick }: ChannelItemProps) { const { activeChannelId } = useChannelsStore(); const isActive = activeChannelId === channel.id; const isMuted = channel.is_muted; return ( ); } 5.4 VISTA DE CANAL (Message List + Input) πŸ”Ή Channel Page // apps/web/src/app/(workspace)/[workspaceDomain]/channel/[channelId]/page.tsx 'use client'; import { ChannelHeader } from '@/components/channels/ChannelHeader'; import { MessageList } from '@/components/messaging/MessageList'; import { MessageInput } from '@/components/messaging/MessageInput'; import { RightPanel } from '@/components/layout/RightPanel'; import { useChannel } from '@/hooks/useChannel'; import { useMessages } from '@/hooks/useMessages'; import { useRightPanelStore } from '@/stores/ui.store'; import { Loader2 } from 'lucide-react'; export default function ChannelPage({ params }: { params: { channelId: string } }) { const { channel, isLoading } = useChannel(params.channelId); const { rightPanelOpen } = useRightPanelStore(); if (isLoading) { return (
); } if (!channel) { return
Channel not found
; } return (
{/* Main column */}
{/* Right panel */} {rightPanelOpen && }
); } πŸ”Ή MessageList (con infinite scroll + virtualization) // apps/web/src/components/messaging/MessageList.tsx 'use client'; import { useEffect, useRef, useCallback } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useMessages } from '@/hooks/useMessages'; import { MessageItem } from './MessageItem'; import { SystemMessage } from './SystemMessage'; import { DateSeparator } from './DateSeparator'; import { useSocket } from '@/hooks/useSocket'; import { Loader2 } from 'lucide-react'; export function MessageList({ channelId }: { channelId: string }) { const { messages, isLoading, hasMore, loadMore, isNewest } = useMessages(channelId); const { socket } = useSocket(); const scrollContainerRef = useRef(null); const bottomRef = useRef(null); const prevScrollHeight = useRef(0); // Virtualization para performance con miles de mensajes const virtualizer = useVirtualizer({ count: messages.length, getScrollElement: () => scrollContainerRef.current, estimateSize: () => 80, // Altura estimada por mensaje overscan: 10, reverse: true, // Mensajes mΓ‘s nuevos arriba (scroll hacia abajo) }); // Auto-scroll al fondo cuando llegan mensajes nuevos useEffect(() => { if (isNewest) { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); } }, [messages.length, isNewest]); // Cargar mΓ‘s mensajes al hacer scroll arriba const handleScroll = useCallback(() => { const container = scrollContainerRef.current; if (!container) return; if (container.scrollTop < 100 && hasMore && !isLoading) { prevScrollHeight.current = container.scrollHeight; loadMore(); } }, [hasMore, isLoading, loadMore]); // Mantener scroll position al cargar mensajes antiguos useEffect(() => { const container = scrollContainerRef.current; if (!container || !isLoading) return; const newScrollHeight = container.scrollHeight; const diff = newScrollHeight - prevScrollHeight.current; container.scrollTop += diff; }, [isLoading]); // Escuchar nuevos mensajes via WebSocket useEffect(() => { if (!socket) return; const handleMessage = (message: MessageEvent) => { if (message.channel_id === channelId) { // Agregar al store useMessagesStore.getState().addMessage(channelId, message); } }; socket.on('message', handleMessage); return () => socket.off('message', handleMessage); }, [socket, channelId]); if (isLoading && messages.length === 0) { return (
); } return (
{/* Channel welcome header */}

#{channel.name}

This is the beginning of the #{channel.name} channel.

{/* Load more indicator */} {hasMore && (
)} {/* Virtualized message list */}
{virtualizer.getVirtualItems().map(virtualItem => { const message = messages[virtualItem.index]; const prevMessage = messages[virtualItem.index + 1]; return (
{/* Date separator */} {shouldShowDateSeparator(message, prevMessage) && ( )} {/* Message or system message */} {message.is_system ? ( ) : ( )}
); })}
{/* Bottom anchor */}
); } // Helpers para agrupar mensajes function shouldShowDateSeparator(msg: Message, prev: Message | undefined): boolean { if (!prev) return false; const msgDate = new Date(msg.ts).toDateString(); const prevDate = new Date(prev.ts).toDateString(); return msgDate !== prevDate; } function shouldShowAvatar(msg: Message, prev: Message | undefined): boolean { if (!prev) return true; if (prev.user_id !== msg.user_id) return true; const diff = new Date(msg.ts).getTime() - new Date(prev.ts).getTime(); if (diff > 5 * 60 * 1000) return true; // MΓ‘s de 5 minutos return false; } πŸ”Ή MessageItem (componente individual de mensaje) // apps/web/src/components/messaging/MessageItem.tsx 'use client'; import { useState, useRef } from 'react'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { PresenceDot } from '@/components/common/PresenceDot'; import { MessageReactions } from './MessageReactions'; import { MessageActions } from './MessageActions'; import { BlockRenderer } from '@/components/block-kit/BlockRenderer'; import { FilePreview } from '@/components/files/FilePreview'; import { formatMessageText } from '@/lib/message-formatter'; import { useUsersStore } from '@/stores/users.store'; import { useSocket } from '@/hooks/useSocket'; import { cn } from '@/lib/utils'; import { Edit2, Trash2, Share, Pin, Bookmark, MoreVertical } from 'lucide-react'; interface MessageItemProps { message: Message; showAvatar: boolean; } export function MessageItem({ message, showAvatar }: MessageItemProps) { const { getUser } = useUsersStore(); const { socket } = useSocket(); const [isHovered, setIsHovered] = useState(false); const [isEditing, setIsEditing] = useState(false); const [editedText, setEditedText] = useState(message.text); const author = getUser(message.user_id); const handleEdit = async () => { // PATCH /api/v1/messages/:id await fetch(`/api/v1/messages/${message.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ text: editedText }), }); setIsEditing(false); }; const handleDelete = async () => { if (!confirm('Delete this message?')) return; await fetch(`/api/v1/messages/${message.id}`, { method: 'DELETE' }); }; const handleSave = async () => { await fetch(`/api/v1/messages/${message.id}/save`, { method: 'POST' }); }; const handlePin = async () => { await fetch(`/api/v1/channels/${message.channel_id}/pins`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message_id: message.id }), }); }; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {/* Avatar */}
{showAvatar ? (
{author?.display_name?.[0]}
) : ( {formatTime(message.ts)} )}
{/* Content */}
{/* Header (name + time) */} {showAvatar && (
{/* mostrar perfil */}} > {author?.display_name} {formatTimestamp(message.ts)} {message.is_edited && ( (edited) )}
)} {/* Message body */} {isEditing ? (