Sistema completo de reservas de pistas de tenis: app Android nativa, backend REST en la nube y panel de administración.
En muchos clubes de tenis pequeños, la reserva de pistas todavía se gestiona por WhatsApp, en una libreta detrás del mostrador, o por llamada telefónica al recepcionista. Esto genera fricciones reales: dos personas reservando la misma franja sin saberlo, no haber dejado claro si la pista era de tierra o dura, dificultad para saber qué pistas están disponibles sin ir físicamente.
PistaGO ataca ese problema con una aplicación móvil que pone la disponibilidad en tiempo real al alcance de cualquier socio del club, evita reservas duplicadas a nivel de servidor, y da al administrador del club una vista completa de uso y métricas.
Aplicación nativa escrita 100% en Kotlin con Jetpack Compose. Arquitectura MVVM + Clean (capas de presentación, dominio y datos). Inyección de dependencias con Hilt. Llamadas HTTP con Retrofit + Gson. Carga asíncrona de imágenes con Coil. Persistencia local de sesión con DataStore.
API REST en Kotlin con Spring Boot 3.5. Autenticación con JWT firmado con HS512 y Spring Security. Datos en PostgreSQL 16 con JPA/Hibernate. Notificaciones push integradas con Firebase Cloud Messaging para avisar a los usuarios de lista de espera cuando una pista se libera. Validaciones de entrada con Bean Validation. Manejo global de errores con un @RestControllerAdvice que devuelve respuestas JSON consistentes.
Desplegado en un VPS de DigitalOcean con Ubuntu 24. Nginx como proxy inverso con HTTPS gestionado por Let's Encrypt. PostgreSQL local en el VPS. Backend ejecutado como JAR de Spring Boot (perfectamente apto para systemd; en este caso por sencillez se usa nohup). APK firmada distribuida públicamente desde nacimiento.me/pistago/.
9 franjas diarias con bloqueo automático de horas pasadas (usando zona horaria de Madrid, independiente del dispositivo). Estados visuales claros: disponible, seleccionada, ocupada o pasada.
Para repartir el uso equitativamente, un usuario solo puede tener una reserva activa simultánea. Los administradores quedan exentos. Validado en el servidor y cubierto con pruebas unitarias.
Si una franja está ocupada, el usuario puede apuntarse en lista de espera. Cuando alguien cancela, el primer usuario en la lista recibe una notificación push de Firebase con 10 minutos para confirmar.
Métricas de uso del club: usuarios, reservas totales, reservas del día, tasa de cancelación, ranking de pistas y usuarios más activos, distribución por día de la semana. Gráfico de barras dibujado con Canvas, sin librerías externas.
El servidor responde a los errores con un JSON consistente (status, error, message, path, errores de validación). El cliente extrae el mensaje real y lo muestra al usuario, no un genérico "Error 400".
Logo, icono de la app, paleta cromática verde y acento lima, tipografía consistente. La marca aparece coherente en todas las pantallas: login, home, listado, perfil con iniciales como avatar...
Tres pantallas representativas del producto final.
Pantalla principal de la app: acceso a reservar, mis reservas, lista de espera y panel de administración.
Panel de estadísticas del administrador: tarjetas resumen, ranking de pistas y usuarios, y gráfico de barras por día de la semana dibujado con Canvas de Compose.
Como la app es de un único cliente confiable, no había necesidad de la complejidad de OAuth. Un JWT firmado con HS512 cumple los requisitos de autenticación y autorización con menos infraestructura. Spring Security gestiona el filtro de autenticación y la verificación del token.
En la pantalla de estadísticas, en lugar de añadir una dependencia como Vico o MPAndroidChart, dibujé el gráfico de barras directamente con Canvas de Jetpack Compose. Para 7 datos (días de la semana) era más limpio y reducía el tamaño de la APK.
Al detectar que los emuladores Android arrancan en UTC por defecto, el bloqueo de franjas pasadas falla si confías en LocalTime.now() sin más. Solución: forzar ZoneId.of("Europe/Madrid") en los cálculos del cliente, así funciona correctamente independientemente de la configuración del dispositivo.
Después de detectar que los repositorios del cliente repetían el mismo patrón de parseo de errores, lo extraje a una función de extensión común Response<T>.extractErrorMessage(). Esto desbloquea que cuando el servidor añade nuevas validaciones, los mensajes aparecen automáticamente en la app sin tocar ningún cliente.
El backend tiene 30 pruebas unitarias propias escritas en JUnit 5 con Mockito Kotlin, distribuidas en los tres servicios principales:
ReservaServiceTest: 13 pruebas (crear, cancelar, validaciones, regla de reserva única, exención de admin).AuthServiceTest: 8 pruebas (registro, login, hash de contraseñas, JWT).ListaEsperaServiceTest: 7 pruebas (apuntarse, salir, posición dinámica, notificación FCM al primero).Slides utilizadas en la defensa del proyecto el 15 de junio de 2026. Puedes verlas online en PDF o descargar el archivo editable de PowerPoint.
Más allá del código, este proyecto fue un ejercicio completo de gestión de un sistema desde cero: tomar decisiones de arquitectura, equilibrar pragmatismo y limpieza, desplegar a producción y mantener una API estable mientras el cliente evoluciona. También me obligó a ser disciplinado con Git (Conventional Commits), tests y documentación, en vez de ir parcheando sobre la marcha.
La parte que más disfruté fue la del diseño visual: trabajar la identidad de marca, dibujar el gráfico de estadísticas con Canvas, y conseguir que la app se sintiera cuidada hasta en los detalles pequeños (placeholder de avatar con iniciales, transiciones suaves entre estados, mensajes de error útiles).
La aplicación está en producción y puedes instalarla en cualquier Android.
Usuario de prueba: usuarioprueba@pistago.com · pistago2026