PrimeFit
PWA offline-first para gestión de gimnasios
La plataforma de gestión de gimnasios que fundé y opero: membresías, retención y una app de entrenamiento que sigue funcionando cuando se cae el Wi-Fi del gimnasio. En producción desde 2024 con un gimnasio fundador, más de 130 miembros y 70 usuarios activos diarios.
Pantallas



Stack tecnológico
Visión general
PrimeFit no empezó como un proyecto para un cliente. Empezó como un problema que veía una y otra vez en gimnasios y entrenadores independientes: el entrenador domina el entrenamiento, pero la operación vive dispersa entre hojas de cálculo, PDFs estáticos y chats. Las rutinas se pierden, los miembros se estancan sin que nadie lo note, y un miembro que se siente ignorado no se queja: simplemente deja de llegar. Lo construí con un gimnasio fundador en Puntarenas, Costa Rica, cuyo dueño perdía cuatro horas cada domingo armando hojas de cálculo y PDFs, y lo diseñé para como se usa de verdad: en el teléfono, entre series, con una mano.
⚡ El desafío
El piso de un gimnasio es un lugar hostil para una app web. La señal se cae a mitad del entrenamiento, el teléfono se bloquea entre series e iOS purga localStorage tras un par de semanas de inactividad. Perder un entrenamiento registrado es el único bug que un miembro no perdona. Además la plataforma es multi-tenant: un gimnasio nunca debe ver los miembros de otro, y un gimnasio suspendido tiene que quedar suspendido de verdad.
✓ La solución
Las lecturas pasan por una caché en Dexie (IndexedDB) que se hidrata al iniciar sesión; las escrituras pasan por una cola durable. Cada operación encolada lleva una idempotency key sellada a la sesión de entrenamiento, y el servidor la hace cumplir con un índice único, así un reintento nunca crea un segundo entrenamiento. Un entrenamiento terminado sin conexión recibe un id local; al sincronizar, las operaciones que dependen de él se remapean al id real, incluso después de reiniciar la app. El service worker se actualiza solo, pero la recarga se difiere mientras haya un entrenamiento en curso o la cola tenga operaciones pendientes. En el servidor, cada tabla está protegida por row-level security con la clave del gimnasio, y las escrituras pasan por RPCs SECURITY DEFINER.
Impacto y resultados
Funcionando desde 2024 con un gimnasio fundador en Puntarenas, Costa Rica
Con 70 usuarios activos diarios registrando entrenamientos en la app
Vitest y React Testing Library más tests de integración a nivel de base de datos
Decisiones clave
Una cola durable en lugar de Redux-Persist
iOS purga localStorage tras 7 a 14 días de inactividad y no tiene transacciones, así que el estado persistido de Redux quedó reducido a un solo slice (el entrenamiento en curso). Todo lo demás se movió a Dexie, con migraciones de esquema versionadas.
Idempotency keys selladas a la sesión
La primera versión generaba una key nueva en cada reintento, así que el índice único no protegía nada. Ahora las keys se crean una vez por sesión de entrenamiento y la base de datos rechaza los duplicados. Se encolan nueve tipos de operaciones, desde terminar un entrenamiento hasta confirmar el pago de una membresía.
Un sync fallido no siempre es un fallo
Las operaciones tienen cinco intentos, pero dos casos no gastan ninguno: una operación que llegó antes que su padre y una sesión expirada. Antes de esa regla, perder la sesión con la app abierta vaciaba la cola en cinco ciclos de polling y le decía al miembro que su entrenamiento se había perdido.
Cero filas no es éxito
PostgREST responde 204 sin error cuando RLS o un soft delete hacen que un UPDATE no toque nada. La cola marcaba la operación como hecha y el dato se evaporaba. Ahora cada escritura pide el id de vuelta y lanza error con cero filas; un barrido encontró el mismo bug en otras cinco features.
Actualizaciones diferidas del service worker
En julio de 2026 todos los clientes quedaron congelados en un bundle viejo: el service worker estaba en modo prompt y ninguna UI mostraba el prompt. Ahora se actualiza solo, pero la recarga espera a que no haya un entrenamiento en curso y la cola esté vacía, y un cliente desactualizado que pide un chunk que ya no existe se recarga como máximo una vez cada 30 segundos.
Una sola puerta pública para los links compartidos
Los entrenadores pueden compartir una rutina de solo lectura sin cuenta. En lugar de agregar una policy para usuarios anónimos, una única función SECURITY DEFINER con una lista de columnas escrita a mano es la única entrada pública, y todos los modos de fallo devuelven el mismo NULL, así que no hay nada que sondear.
Destacados técnicos
- ▸ React 18, TypeScript, Vite y slices de Redux Toolkit con createAsyncThunk sobre RPCs de Supabase; 16 áreas funcionales, desde entrenamientos y analíticas hasta cobros y un nivel de superadmin
- ▸ Service worker propio con Workbox (injectManifest) y un build de respaldo con paridad verificada por tests
- ▸ Row-level security en cada tabla con la clave del gimnasio (más de 200 policies); tras descubrir que el admin de un gimnasio podía quitarse su propia suspensión, las columnas sensibles solo se escriben mediante RPCs SECURITY DEFINER
- ▸ Radar de retención, alertas de estancamiento con una cascada de umbrales de cinco niveles resuelta en SQL, rachas semanales, check-in por QR y kiosco, cobro de membresías con SINPE
- ▸ 11 Edge Functions de Supabase (web push, invitaciones, reporte mensual al dueño) y pg_cron
- ▸ Una suite de tests offline deliberadamente hostil: sus nueve casos fallidos, todos con el mismo final de datos perdidos en silencio, se convirtieron en la lista de arreglos
- ▸ Toda la UI migrada de MUI a shadcn/ui y Tailwind v4 sin quitar una sola funcionalidad, bajo una regla escrita: el diseño se adapta a la funcionalidad, nunca al revés
Aprendizajes clave
Offline-first es una decisión de producto, no una bandera de caché. Lo difícil no es guardar datos, es decidir qué significa un reintento.
Una garantía que solo vive en el cliente no es una garantía. El índice único es lo que de verdad evita entrenamientos duplicados cuando hay dos pestañas abiertas.
Hay que probar a propósito el fallo que más se teme. La suite offline hostil encontró nueve formas de perder datos en silencio que ningún test del camino feliz habría tocado.
Suspender un tenant tiene que hacerse cumplir en un lugar real. Los permisos por columna son tan parte del multi-tenant como las policies por fila.
Diseñar con un dueño de gimnasio real, con el teléfono en la mano entre series, resolvió más discusiones de UI que cualquier mockup.