El Concepto
ChuletonApp nació de una necesidad personal: quería competir con mis amigos en carreras y ciclismo de forma divertida, con ligas, puntos y badges. Como si el ejercicio fuera un videojuego.
La idea era simple:
- Sincronizas tus actividades de Garmin/Strava
- Creas ligas privadas con amigos
- Sistema de puntos ponderado por tipo de actividad
- Rankings semanales/mensuales
- Badges y achievements
Tech Stack V2
# Backend
Rails 7.1
PostgreSQL 15
Sidekiq (jobs en background)
Railway (hosting)
# Frontend
Turbo + Stimulus
Tailwind CSS
# Integraciones
Garmin Connect API
OAuth 2.0
El Problema: Cambios de Strava
En Noviembre 2024, Strava cambió completamente sus términos de API. Las nuevas políticas prohíben:
🚫 Mostrar actividades de otros usuarios en rankings
🚫 Monetizar features que usen datos de Strava
🚫 Uso de datos para ML/AI
🚫 Tablas de clasificación públicas
Impacto en ChuletonApp
Mi concepto completo violaba los nuevos términos. No podía:
- Crear ligas donde Usuario A ve actividades de Usuario B
- Cobrar por features premium con datos Strava
- Mostrar rankings públicos
La app era inviable con Strava.
El Pivot: All-in con Garmin
¿Por qué Garmin?
Investigué a fondo y descubrí que Garmin Connect API:
✅ Es 100% gratuita para desarrolladores de negocio
✅ Permite mostrar rankings y leaderboards
✅ No prohíbe monetización
✅ Soporte para features sociales
✅ No tiene historial de cambios restrictivos como Strava
Comparación de Políticas
| Feature | Strava (2024) | Garmin |
|---|---|---|
| Rankings públicos | ❌ Prohibido | ✅ Permitido |
| Monetización | ❌ No | ✅ Sí |
| Mostrar datos de otros | ❌ No | ✅ Sí (con consent) |
| Coste API | Gratis | Gratis |
| Restricciones comerciales | Muy altas | Bajas |
Implementación: OAuth con Garmin
1. Registro en Garmin Developer Program
Proceso sorprendentemente sencillo:
- Completar formulario online
- Justificar uso comercial
- Aprobación en 2 días laborables
2. Flujo OAuth 2.0
# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :garmin,
ENV['GARMIN_CONSUMER_KEY'],
ENV['GARMIN_CONSUMER_SECRET'],
scope: 'activities,profile'
end
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def garmin_callback
auth = request.env['omniauth.auth']
user = User.find_or_create_by(garmin_user_id: auth.uid) do |u|
u.email = auth.info.email
u.name = auth.info.name
u.garmin_token = auth.credentials.token
u.garmin_secret = auth.credentials.secret
end
session[:user_id] = user.id
redirect_to dashboard_path
end
end
3. Sincronización de Actividades
class GarminSyncJob < ApplicationJob
queue_as :default
def perform(user_id)
user = User.find(user_id)
activities = GarminAPI.fetch_activities(
token: user.garmin_token,
secret: user.garmin_secret,
since: 7.days.ago
)
activities.each do |activity_data|
Activity.find_or_create_by(
user: user,
external_id: activity_data['activityId']
) do |a|
a.activity_type = map_activity_type(activity_data['activityType'])
a.distance = activity_data['distance'] # metros
a.duration = activity_data['duration'] # segundos
a.started_at = activity_data['startTimeGMT']
a.calories = activity_data['calories']
end
end
end
private
def map_activity_type(garmin_type)
{
'running' => 'run',
'cycling' => 'ride',
'swimming' => 'swim',
'walking' => 'walk'
}[garmin_type.downcase] || 'other'
end
end
Sistema de Puntos Ponderado
Una de las features clave es el sistema de scoring:
class PointsCalculator
WEIGHTS = {
'run' => 2.0, # Correr puntúa doble
'ride' => 1.0, # Ciclismo base
'swim' => 2.5, # Nadar puntúa más (es duro!)
'walk' => 0.5, # Caminar puntúa menos
'other' => 1.0
}
def self.calculate(activity)
base_points = activity.distance / 1000.0 # Puntos = km
weight = WEIGHTS[activity.activity_type] || 1.0
(base_points * weight).round(2)
end
end
# Ejemplo:
# 10km corriendo = 10 * 2.0 = 20 puntos
# 50km ciclismo = 50 * 1.0 = 50 puntos
# 2km nadando = 2 * 2.5 = 5 puntos
Arquitectura de Ligas
class League < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
has_many :activities, through: :users
def current_ranking
memberships
.joins(user: :activities)
.where('activities.started_at > ?', current_period_start)
.group('memberships.id')
.select('memberships.*, SUM(activities.points) as total_points')
.order('total_points DESC')
end
private
def current_period_start
case period_type
when 'weekly' then Date.today.beginning_of_week
when 'monthly' then Date.today.beginning_of_month
when 'all_time' then created_at
end
end
end
Cumplimiento GDPR
Aspecto crítico que no podía ignorar:
Consentimiento Explícito
# Al unirse a una liga
class JoinLeagueForm
def consent_text
<<~TEXT
Al unirte a esta liga, aceptas:
☑ Compartir tus actividades con otros miembros
☑ Que tus datos se muestren en rankings
☑ Que podamos procesar tus datos de Garmin
Puedes abandonar la liga en cualquier momento.
TEXT
end
end
Privacy Policy
Implementé usando freeprivacypolicy.com con:
- Qué datos procesamos (nombre, actividades, email)
- Por qué (gamificación de ligas)
- Cómo se almacenan (PostgreSQL encriptado)
- Con quién se comparten (solo miembros de tu liga)
- Derechos del usuario (acceso, eliminación, portabilidad)
Deployment en Railway
Railway resultó ser perfecto para este proyecto:
# railway.json
{
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"startCommand": "bin/rails server",
"healthcheckPath": "/health",
"restartPolicyType": "ON_FAILURE"
}
}
Costes mensuales:
- Starter plan: $5/mes (suficiente para beta)
- PostgreSQL: Incluido
- 500GB transfer: Incluido
Estado Actual (Enero 2025)
Beta Testing
- 12 usuarios activos (amigos y familia)
- 3 ligas funcionando
- ~200 actividades sincronizadas
Métricas Clave para Decisión GO/NO-GO (Diciembre 2025)
Estoy midiendo:
- Engagement semanal: Meta >60%
- Retención mes 1: Meta >50%
- NPS: Meta >40
Si no llego a estas métricas, cierro el proyecto y aprendo las lecciones.
Próximos Pasos
Si métricas SON positivas:
-
Fase Freemium (Q1 2026)
- Free: 1 liga, ranking básico
- Pro: €4.99/mes - Ligas ilimitadas, badges, analytics
-
Marketing (Q2 2026)
- Reddit (r/running, r/cycling)
- Product Hunt launch
- Indie Hackers community
-
Features Premium
- Challenges personalizados
- Integración con más plataformas (Terra API si escala)
- Estadísticas avanzadas
Si métricas NO son positivas:
Shutdown ordenado:
- Avisar usuarios con 30 días de antelación
- Ofrecer export de datos
- Documentar aprendizajes (escribir postmortem en este blog)
Lecciones hasta Ahora
✅ Aciertos
- Investigar términos de API ANTES de construir - Me salvó meses de trabajo
- Garmin vs Strava - Mejor decisión del proyecto
- GDPR desde día 1 - Evitar problemas futuros
- Métricas claras - Sé cuándo pivotar o parar
❌ Errores
- Empecé con Strava - Perdí 2 semanas antes del pivot
- Diseño inicial muy complejo - Debí empezar más simple
- No validé concepto antes - Asumí que amigos querrían competir
🎓 Aprendizajes Técnicos
- OAuth 2.0 con múltiples providers
- Background jobs con Sidekiq
- Webhook handling para sync en tiempo real
- Railway deployment workflow
- GDPR compliance práctico
Reflexión
Este proyecto me está enseñando mucho sobre product-market fit y validación.
No importa lo bien que esté construido técnicamente si nadie lo usa. Por eso priorizo métricas sobre features.
Diciembre 2025 será el momento de verdad. Go big or go home.
Estado: Beta privada
Stack: Rails 7.1 + PostgreSQL + Garmin API
Deployment: Railway
Decisión GO/NO-GO: Diciembre 2025
Repo: Privado (por ahora)
¿Construyendo tu propia SaaS? Conectemos - me encantaría escuchar tu historia.