Saltar al contenido
PG
Todos los proyectos

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

pulse · vista previa
Datos de demostración. Chat de un canal en la web de Next.js: lista de canales a la izquierda, mensajes con reacciones y respuestas, y el campo para escribir.
Datos de demostración. La web de Next.js a 390 px de ancho, con el chat a pantalla completa.

Funcionalidades

Implementado

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.

Datos de demostración. El mismo chat en modo oscuro, con reacciones y un mensaje respondido.
Implementado

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

Implementado

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.

Datos de demostración. Paleta Ctrl+K abierta sobre el chat, con canales y workspaces filtrados por lo escrito.
Parcial

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.

Datos de demostración. La app móvil hecha con Ionic mostrando un canal con mensajes.
Implementado

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.

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.

Arquitectura de PulseTres clientes (Angular, Next.js e Ionic) usan la misma API REST y el mismo WebSocket STOMP de Spring Boot. Angular pasa por nginx; Next.js reenvía /api con rewrites. La aplicación guarda los datos en PostgreSQL, la presencia y los contadores en Redis y los archivos en RustFS, compatible con S3.ClientesAngular 22cliente completoNext.js 16React 19Ionic 9CapacitorHTTPS · WSEdgenginx · rewritesIonic llama directoREST · STOMPSpring Boot 3REST + STOMP11 módulos · eventosidentitymessagesrealtimefilesJPA · Redis · S3DatosPostgreSQLFlyway · pg_trgmRedispresencia · rate limitRustFSarchivos (S3)
  • 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: messages publica UserMentionedEvent y 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 /api con 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
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 ChannelInterceptor valida el JWT en el CONNECT y comprueba el acceso en cada SUBSCRIBE; cualquier otro destino se rechaza.
Por qué
Las mismas reglas de acceso del REST se aplican al tiempo real.
Código
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
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
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.