MoviFit

Reservas de clases y membresías para estudios de movimiento

Reservas, membresías por créditos y pagos para estudios de pilates, yoga y barre. Pura Vida Moves, un estudio boutique en Guanacaste, opera con él 25 clases semanales y 61 alumnos matriculados, con las reglas de negocio aplicadas en PostgreSQL y probadas con pgTAP.

Rol: Fundador & Full Stack Developer
Duración: 2026 - Presente
MoviFit hero image

Pantallas

Sitio público de Pura Vida Moves, renderizado por MoviFit en su propio subdominio

Stack tecnológico

React TypeScript Vite TanStack Router TanStack Query Supabase PostgreSQL pgTAP Playwright Astro Cloudflare Workers Tailwind CSS

Visión general

Pura Vida Moves es un estudio boutique de movimiento en Guanacaste, Costa Rica: 16 clases, 25 bloques semanales, 8 instructores, adultos y niños. La matrícula, los pagos y la asistencia se llevaban por WhatsApp y a mano. MoviFit reemplazó eso. Los alumnos reservan clases y compran paquetes desde el teléfono, los encargados gestionan las clases de sus hijos, los instructores pasan lista, una tablet en la entrada funciona como kiosco de check-in y el estudio tiene su propio sitio público. Empecé el repositorio a finales de agosto de 2026, y en cuestión de semanas el estudio ya operaba reservas y pagos reales con él.

⚡ El desafío

Un crédito de clase es dinero. Los alumnos compran paquetes que vencen, regalan créditos a amigos, cancelan tarde o no llegan, y cada uno de esos casos tiene que poder explicársele a un cliente meses después. Dos personas pueden intentar tomar el último cupo en el mismo segundo. Una tablet compartida en la recepción debe permitir que cualquiera marque asistencia sin exponer nada de nadie. Y el sitio público nunca debe convertirse en una forma de leer la base de datos.

✓ La solución

Primero escribí la arquitectura como ADRs, 14 hasta ahora, cada uno con la alternativa que descarté. El navegador solo lee y llama funciones: cada escritura es un RPC de PostgreSQL, así las reglas, las transacciones y la bitácora de auditoría viven en un solo lugar. Los créditos son un ledger append-only; reservar retiene un crédito y asistir lo consume; el último cupo se decide con un único UPDATE condicional en la base de datos. Todo se prueba donde vive, con pgTAP, y la carrera por el cupo se prueba con diez conexiones en paralelo peleando por un asiento.

Impacto y resultados

61
Alumnos matriculados

En Pura Vida Moves, reservando clases y gestionando membresías mensuales desde la app

25
Clases por semana

16 clases y 8 instructores en dos programas

851
Aserciones pgTAP

55 archivos de tests de base de datos en CI, junto a Vitest y Playwright

Decisiones clave

1

SPA en React, no Next.js (ADR-001)

La lógica de negocio vive en Postgres, así que lo único que Next.js aportaría es SSR para SEO, al costo de server y client components y capas de caché. El SEO se fue a un sitio aparte en Astro.

2

Los créditos son un ledger, no un contador (ADR-002)

El saldo es la suma de las filas no vencidas, y se gasta primero lo que vence primero. Nunca se edita ni se borra nada: una corrección es una fila nueva. Hay triggers que bloquean UPDATE y DELETE, y un trigger a nivel de sentencia que bloquea TRUNCATE, que los triggers por fila nunca ven.

3

Reservar retiene el crédito, asistir lo gasta (ADR-006)

Una cancelación tardía o una ausencia solo cuesta una clase si la clase estaba llena, porque es cuando dejó a alguien más por fuera. La carrera por el último cupo se resuelve con un UPDATE ... WHERE reserved_count < capacity, no en React.

4

El cliente lee; escribir es llamar una función (ADR-011)

Row-level security responde qué fila, no qué columna. Una auditoría encontró que un permiso de UPDATE sobre toda la tabla dejaba a un cliente mover a un alumno a otra organización. El permiso se redujo, y ahora cualquier insert, update o delete en el frontend tiene que justificarse en el pull request.

5

Un kiosco con un rol mínimo (ADR-008)

La tablet de la recepción guarda un token de dispositivo, no una sesión de administrador. Puede hacer exactamente tres cosas: listar las sesiones de hoy, buscar alumnos activos por nombre e insertar una fila de asistencia. Se reinicia a los 10 segundos para que la siguiente persona nunca vea el nombre anterior.

6

El sitio público ve una sola vista (ADR-013)

Cada estudio tiene un sitio en su propio subdominio, renderizado con Astro en Cloudflare Workers. Solo puede leer una vista de Postgres y cuatro funciones públicas, así que la pregunta de qué puede ver el sitio público se responde en una pantalla. Una caída de la base de datos devuelve 503, nunca 404, para que Google no dé de baja al estudio.

Destacados técnicos

Aprendizajes clave

Una alumna escribió que le habían rebajado una clase dos veces, una como ausencia y otra como asistencia. El arreglo fue una fila nueva en el ledger que devuelve el crédito en la misma transacción, más una migración idempotente que le repuso el crédito a todos los ya afectados, demostrada corriéndola dos veces en un test.

Di EXECUTE a authenticated y asumí que los anónimos quedaban fuera. No era así: Postgres da EXECUTE a PUBLIC por defecto, y dos de mis tres arreglos solo parecían haber funcionado. Los privilegios se miden, no se asumen.

Un correo de confirmación salió con un link roto. Los logs de la función mostraron que la versión que atendía producción nunca se había construido desde el repositorio. Todo lo que se despliega a mano termina contradiciendo a git.

El link para registrar el kiosco solo se podía copiar en la computadora de la administradora, nunca en la tablet al otro lado del salón. El único camino que funcionaba era el de mi test end-to-end. Un código QR lo resolvió.

Escribir la alternativa descartada junto a cada decisión es lo que permite revisar esa decisión un mes después.

Ver todos los casos de estudio