Pulse
Workspaces, canales y mensajes en directo al estilo Slack, con un backend y tres clientes: Angular, Next.js e Ionic.
En 30 segundos
- Categoría
- Full stack · Comunicación en tiempo real
- Estado
- Backend y web Angular completos · Next.js e Ionic parciales · sin demo pública
- Stack
- Spring Boot 3 · WebSocket/STOMP · PostgreSQL · Redis · S3 (RustFS)
- Clientes
- Angular 22 (completo) · Next.js 16 + React 19 · Ionic 9 + Capacitor
- Tests
- 117 en el backend con PostgreSQL, Redis y RustFS reales · 8 en Angular · 10 en Next.js
- Entrega
- Docker Compose con 5 servicios y healthchecks · CI con 5 jobs
- Demo online
- Sin demo pública
Visión general
- Qué es
- Una plataforma de comunicación en tiempo real: comunidades (workspaces) con canales públicos y privados, mensajes en directo, mensajes privados, notificaciones, archivos y búsqueda.
- Qué problema resuelve
- Un chat en equipo tiene que ser instantáneo sin dejar de ser fiable: ningún mensaje puede perderse, aparecer duplicado o llegar a quien no tiene acceso al canal.
- Para quién
- Equipos y comunidades que se organizan por espacios de trabajo y canales, con roles de moderación.
- Objetivo
- Demostrar que una misma API REST y un mismo WebSocket pueden servir a tres clientes distintos con las mismas reglas de seguridad.
Vista previa


Funcionalidades
Mensajería en tiempo real
Mensajes, ediciones, borrados, reacciones, indicador de "está escribiendo" y presencia online/offline llegan al instante a quien está suscrito al canal.
Para qué sirve: La conversación se siente en directo sin recargar nada.

Workspaces con roles y moderación
Cinco roles (OWNER, ADMIN, MODERATOR, MEMBER, GUEST). Solo se puede expulsar, banear o cambiar el rol de alguien por debajo de ti, y los canales de anuncios solo admiten mensajes de moderadores.
Para qué sirve: Cada comunidad se gobierna sola sin que nadie pueda escalar permisos.
Recurso pendienteCaptura pendiente: miembros y registro de auditoría en Angular
Búsqueda y paleta de comandos
Búsqueda por texto parcial con filtros combinables (workspace, canal, autor, fechas, menciones) en Angular. En Next.js, una paleta Ctrl+K para saltar entre canales y workspaces.
Para qué sirve: Encontrar un mensaje o un canal sin recorrer la lista.

Tres clientes sobre la misma API
Angular es el cliente completo. Next.js cubre login, registro, workspaces y chat en tiempo real. La app Ionic cubre el uso diario: workspaces, canales, chat con reacciones, mensajes privados y notificaciones.
Para qué sirve: Demuestra que la API no depende de ningún cliente.

Archivos validados por su contenido
Imágenes, PDF, txt, docx y xlsx de hasta 10 MB. El tipo se comprueba por los primeros bytes, la clave de almacenamiento la genera el servidor y cada descarga comprueba el acceso al canal.
Para qué sirve: Renombrar un .exe a .png no sirve, y conocer el id de un archivo no da acceso a él.
Galería
Recurso pendienteVídeo pendiente: dos usuarios conversando en tiempo real
Stack
Backend
- Java 21 + Spring Boot 3.3
- API REST y WebSocket en una sola aplicación
- Spring WebSocket + STOMP
- Temas por canal y suscripciones autorizadas una a una
- Spring Security + jjwt
- Access token de 15 min y refresh token revocable
- MapStruct + Lombok
- Mapeo entidad → DTO sin código repetido
- springdoc-openapi
- Swagger UI
Datos
- PostgreSQL + Flyway
- 13 migraciones; búsqueda con índice trigram (
pg_trgm) - Redis
- Presencia y rate limiting
- RustFS (API S3) + cliente MinIO
- Almacenamiento de archivos
Clientes
- Angular 22 + signals + RxJS
- Web completa
- Next.js 16 + React 19
- Segunda web, App Router y CSS Modules
- Ionic 9 + Capacitor 8
- App móvil (Android)
- @stomp/stompjs
- Tiempo real en los tres clientes
Testing
- JUnit 5 + MockMvc
- Tests de comportamiento de la API
- Testcontainers
- PostgreSQL, Redis y RustFS reales
- Vitest
- Interceptor, cliente de la API y lógica del chat
Infraestructura
- Docker Compose
- 5 servicios con healthchecks
- nginx
- Estáticos y proxy de /api y /ws
- GitHub Actions
- Backend, Angular, Next.js, móvil e imágenes
Arquitectura
Un monolito modular de Spring Boot con once módulos por dominio, tres clientes y tres almacenes de datos, cada uno para lo que mejor sabe hacer.
- Los módulos (
identity,workspaces,channels,messages,realtime…) se usan entre sí solo a través de sus servicios, nunca de sus repositorios. - Los efectos secundarios son eventos de dominio:
messagespublicaUserMentionedEventy notificaciones, auditoría y tiempo real escuchan. - PostgreSQL guarda lo permanente; Redis, lo efímero (presencia, contadores); RustFS, los archivos.
- Angular llega a través de nginx; Next.js reenvía
/apicon rewrites y abre el WebSocket directo al backend; Ionic habla con la API directamente.
Decisiones técnicas
Cada decisión con el problema, la solución, el motivo y el lugar exacto del código donde está.
Escribir por REST, difundir tras el commit
- Problema
- Si los mensajes se guardan por el WebSocket, hay dos vías de validación y autorización. Si se difunden antes de confirmar la transacción, se puede anunciar un mensaje que luego se deshace.
- Solución
- Los mensajes se guardan por REST, y un listener con
@TransactionalEventListener(AFTER_COMMIT)los difunde por STOMP. El cliente descarta duplicados por id. - Por qué
- Una sola vía de escritura y el WebSocket nunca es la fuente de verdad: si se cae, no se pierde nada.
- Código
- MessageEventListener.java:26 (se abre en una pestaña nueva)backend/src/main/java/com/pulse/realtime/listener/MessageEventListener.java
- Test
“messageSentOverRest_arrivesInRealTimeToChannelSubscribers”
WebSocketIT.java:74 (se abre en una pestaña nueva)
Cada suscripción se autoriza
- Problema
- Validar el token al conectar no basta: un usuario autenticado podría suscribirse al tema de un canal privado ajeno solo conociendo su id.
- Solución
- Un
ChannelInterceptorvalida el JWT en elCONNECTy comprueba el acceso en cadaSUBSCRIBE; cualquier otro destino se rechaza. - Por qué
- Las mismas reglas de acceso del REST se aplican al tiempo real.
- Código
- WebSocketAuthInterceptor.java:48 (se abre en una pestaña nueva)backend/src/main/java/com/pulse/realtime/config/WebSocketAuthInterceptor.java
- Test
“userOutsideTheWorkspace_doesNotReceiveChannelMessages”
WebSocketIT.java:89 (se abre en una pestaña nueva)
Presencia en Redis con caducidad
- Problema
- Si el servidor se cae sin cerrar las sesiones, los usuarios se quedan "online" para siempre. Y una persona puede estar conectada desde la web y el móvil a la vez.
- Solución
- Un sorted set por usuario en Redis, con una entrada por sesión y su caducidad como puntuación, renovada cada 30 s. El usuario está online mientras le quede alguna sesión viva.
- Por qué
- Las sesiones de un servidor caído caducan solas, y funciona con varias instancias del backend.
- Código
- PresenceService.java:73 (se abre en una pestaña nueva)backend/src/main/java/com/pulse/realtime/service/PresenceService.java
- Test
“userWithTwoSessions_staysOnlineUntilTheLastOneCloses”
WebSocketIT.java:146 (se abre en una pestaña nueva)
Tipo de archivo por sus primeros bytes
- Problema
- El tipo MIME y la extensión los decide el cliente, así que no son fiables.
- Solución
- Se leen las firmas del contenido (PNG, JPEG, PDF, ZIP de Office…) y se rechaza el archivo si no coincide con lo declarado. La subida pasa por el backend en lugar de usar URLs prefirmadas.
- Por qué
- Permite validar el contenido real y comprobar permisos en cada descarga, a cambio de que los archivos pasen por el servidor.
- Código
- FileTypeValidator.java:22 (se abre en una pestaña nueva)backend/src/main/java/com/pulse/files/service/FileTypeValidator.java
- Test
“fileWhoseContentDoesNotMatchItsDeclaredType_isRejected”
FileControllerIT.java:99 (se abre en una pestaña nueva)
Monolito modular, no microservicios
- Problema
- Separar en microservicios añade red entre servicios, consistencia eventual y despliegues múltiples.
- Solución
- Una sola aplicación organizada por dominios, con reglas de dependencia entre módulos y eventos para los efectos secundarios.
- Por qué
- Para este tamaño, los microservicios solo añadirían coste. La separación por módulos permite extraer uno más adelante si hiciera falta.
Lo que falta
- Next.js no tiene mensajes privados, búsqueda ni administración; Ionic no tiene búsqueda, edición, adjuntos ni moderación.
- Con varias instancias del backend, el broker STOMP en memoria debería pasar a uno externo (RabbitMQ o un relay).
- Los archivos subidos y nunca enviados no se limpian solos, y no hay subida de avatar.
- Faltan tests end-to-end de los clientes.