GrowTogether
Una app de hábitos con componente social, inspirada en Atomic Habits de James Clear: construye hábitos consistentes, visualiza tu progreso y compite con amigos en desafíos.
- Cuándo
- 2025/2026 · Trabajo Final de Grado de DAM
- Rol
- Todo el proyecto, en solitario
- Stack
- Java 17
- Spring Boot 3.3
- PostgreSQL
- JWT
- Flutter 3.38
- Dart 3.10
- Provider
- Docker
- Enlaces
- GrowTogetherAPI Backend REST — autenticación, hábitos, desafíos, administración
- GrowTogetherAPP App móvil que usan los usuarios finales
- GrowTogetherADMIN Panel de administración web, uso interno
- GrowTogetherDATA Paquete Dart compartido entre app y panel
- Memoria del proyecto PDF de 59 páginas: análisis, arquitectura, decisiones y despliegue
El proyecto ya no está desplegado: no hay demo en línea ni APK que descargar. El código completo está en los cuatro repositorios, y la memoria explica cómo funcionaba por dentro.
Qué es
- Autenticación JWT con revocación de tokens y rate limiting propio
- Hábitos con rachas, historial y heatmap de actividad hecho desde cero
- Desafíos entre amigos y consejos diarios
- Panel de administración con auditoría de acciones
- App en tres idiomas: español, inglés y catalán
Cómo funciona, en vídeo
Patu recorre el proyecto por dentro: los cuatro repositorios, el camino de una petición, la seguridad y cómo llega todo a producción.
Transcripción del vídeo
GrowTogether por dentro
Llevas una racha de siete días… y el octavo se te olvida. GrowTogether es una app de hábitos con rachas y desafíos con amigos. Es el trabajo de fin de grado de Jordi. Soy Patu, y te enseño cómo funciona por dentro.
Cuatro repositorios
Por dentro son cuatro repositorios. La API, en Java con Spring Boot, es el motor y guarda los datos en PostgreSQL. La app móvil y el panel de administración están hechos en Flutter. Y entre los dos y la API hay un paquete compartido, que se llama DATA.
Las capas de la API
La API sigue siempre el mismo camino: controlador, servicio y repositorio. El controlador recibe la petición y valida lo que llega. El servicio decide qué hacer. Y el repositorio es el único que habla con la base de datos. Los datos salen en DTOs, nunca como entidades, para no enseñar campos internos como la contraseña.
Una petición, paso a paso
Veamos qué pasa cuando abres la app. Haces login y la API te devuelve un token JWT, firmado y válido durante veinticuatro horas. En cada petición siguiente, el token viaja en la cabecera Authorization. Antes de llegar a ningún controlador, la petición pasa por dos filtros: primero el límite de peticiones, y después el del token. Si el token es válido, entras. Si no, un 401. Y todo lo que cuelga de admin exige además el rol de administrador.
El límite de peticiones
El primer filtro cuenta los intentos de login y de registro. Diez por minuto, por cada IP. El undécimo recibe un 429: demasiados intentos. Así alguien no puede probar contraseñas sin parar.
Revocar un token
Un JWT, una vez firmado, es válido hasta que caduca. ¿Y si cambias la contraseña? Dentro del token va un número: la versión del token. Y el usuario guarda la suya en la base de datos. Cuando cambias tu contraseña, un admin te la resetea o te bloquea, esa versión sube uno. El token viejo trae la versión antigua, no coincide, y deja de valer.
Rachas y puntos
Cada vez que marcas un hábito, la API recalcula tu racha. Si el hábito es de ciertos días, como lunes, miércoles y viernes, los otros días no cuentan ni la rompen. Y cada noche, a las 00:01, una tarea marca como no completado todo lo que quedó pendiente el día anterior. Los puntos se ganan en los desafíos: diez por día, y un diez por ciento más por cada día de racha, hasta un máximo de triple.
DATA, el paquete compartido
La app y el panel hablan con la misma API. Para no escribirlo todo dos veces, existe DATA. Tiene los modelos, el cliente HTTP y los repositorios. Un interceptor pone el token en cada petición, y si la API responde 401, borra la sesión guardada. Cualquier cambio en un endpoint se toca en un solo sitio. Y las apps lo importan por versión, con una etiqueta de git: ahora mismo, la 0.5.0.
La app móvil
La app usa Provider: diez gestores de estado, separados de la interfaz. Los recordatorios son notificaciones locales, programadas en el propio móvil. Si pierdes la conexión, un banner te avisa y el panel principal se sirve de una copia guardada.
El panel de administración
El panel es Flutter Web. Al entrar, comprueba que tu rol sea administrador; si no, descarta el token. Desde ahí puedes bloquear usuarios, pero siempre con un motivo, y queda anotado en el registro de auditoría. También gestionas los consejos del día y ves las métricas.
Del commit a producción
Cuando Jordi sube cambios a la rama principal, la API se despliega sola. GitHub construye la imagen de Docker, la sube a un registro y se conecta por SSH al servidor de AWS para levantarla. En DATA, cada cambio pasa por análisis estático y tests antes de aceptarse.
Cierre
Y esto es GrowTogether por dentro. El código de los cuatro repositorios es público, en GitHub: devPatuel. Soy Patu. Hasta la próxima.
Arquitectura
Por dentro son cuatro repositorios: la app y el panel de administración se apoyan en una API propia y un paquete de datos compartido.
Pulsa una pieza del diagrama para ver qué hace y con qué se conecta.
- APP
- App Flutter que usan los usuarios finales: hábitos con rachas y un heatmap de actividad hecho a mano, desafíos entre amigos y consejos diarios, en español, inglés y catalán. Sigue funcionando sin cobertura leyendo de una caché local. Por qué el heatmap es propio · Por qué no se cae sin conexión
- ADMIN
- Panel de administración en Flutter Web, de uso interno, con auditoría de las acciones del equipo. Comparte toda su lógica de datos con la app a través de DATA.
- DATA
- Paquete Dart compartido entre la app y el panel: es el único que habla con la API, así ambos consumen hábitos, desafíos y datos de usuario de la misma forma, sin duplicar lógica.
- API
- Backend en Spring Boot: autenticación, hábitos, desafíos y lo que necesita el panel de administración. Un job programado a medianoche rellena el historial para que las estadísticas del día siguiente estén listas. Por qué no hay blacklist de tokens · Por qué no hay huecos en el historial
Capturas
Decisiones técnicas
Cada decisión de arquitectura quedó documentada con las alternativas que descarté y por qué.
- Revocar sesión sin tabla extra
- Un campo
tokenVersionen vez de una blacklist en base de datos o Redis: revocación inmediata sin romper el modelo stateless de JWT. - Heatmap de actividad hecho desde cero
- En vez de un paquete de terceros, construí el calendario de calor a mano para controlar colores, tamaños y la diferencia entre «no completado» y «día futuro».
- Historial sin huecos, sin filas de sobra
- Los días sin interacción no generan registro; una tarea programada a medianoche rellena los huecos para que las estadísticas del día siguiente estén listas.
- La app no se cae sin cobertura
- Lectura tolerante a red con caché local; las acciones que sí necesitan conexión avisan con claridad. Sin la complejidad de una cola de sincronización offline completa.
- Romper un ciclo de dependencias
- Separé el
PasswordEncoderde la configuración de seguridad para evitar un ciclo real entre Spring Security y el servicio de usuarios.
Más detalles del proyecto
Todo lo que hay en esta página sale de la memoria que presenté: los requisitos, los casos de uso, los diagramas de clases y de base de datos, las pruebas y el despliegue, con el porqué de cada decisión.