← Volver a proyectos

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
Enlaces

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

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.

Las capturas son de la app con datos de prueba. El despliegue que se explica es el que usaba el proyecto mientras estuvo en línea.
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.

APP Flutter · Android ADMIN Flutter Web DATA paquete Dart API Spring Boot
Un mismo paquete de datos alimenta la app y el panel; solo él habla con la API.
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

Pantalla de inicio de la app: progreso del día y lista de hábitos, cada uno con su racha.
Los hábitos del día, con la racha de cada uno.
Formulario de nuevo hábito: selector de hábito positivo o negativo, nombre, descripción y una rejilla de iconos.
Crear un hábito, positivo o negativo, con su icono.
Pantalla de análisis: mapa de calor del año, totales de hábitos completados y ranking de mejores rachas.
Mapa de calor del año y récords de racha.
Pantalla de desafíos: retos activos con sus participantes, días restantes y barra de progreso.
Desafíos en grupo, con progreso y participantes.
Detalle de un desafío: botón de hecho hoy con la racha y un podio con los tres participantes que más puntos llevan.
El podio del desafío, con los puntos de cada participante.
Pantalla de perfil con el selector de idioma abierto: castellano, inglés y valenciano.
La app está en castellano, inglés y valenciano.
La misma pantalla de inicio con un aviso rojo de sin conexión, mostrando los datos guardados en el dispositivo.
Sin conexión: la app avisa y sigue mostrando los datos del dispositivo.
Panel de administración web: tarjetas con usuarios activos, hábitos creados y desafíos, y un gráfico de altas por mes.
El panel de administración, en web: métricas de uso y altas por mes.
Panel de administración: tabla de consejos diarios con fecha, título, descripción y estado, y pestañas de lista y calendario.
Los consejos diarios se programan desde el panel, en lista o en calendario.

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 tokenVersion en 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 PasswordEncoder de 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.