ChuletonApp V2: El pivot de Strava a Garmin API

La historia de cómo construí una app de fitness gamificada, los cambios restrictivos de Strava, y por qué pivotamos completamente a Garmin Connect API.

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

FeatureStrava (2024)Garmin
Rankings públicos❌ Prohibido✅ Permitido
Monetización❌ No✅ Sí
Mostrar datos de otros❌ No✅ Sí (con consent)
Coste APIGratisGratis
Restricciones comercialesMuy altasBajas

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:

  1. Fase Freemium (Q1 2026)

    • Free: 1 liga, ranking básico
    • Pro: €4.99/mes - Ligas ilimitadas, badges, analytics
  2. Marketing (Q2 2026)

    • Reddit (r/running, r/cycling)
    • Product Hunt launch
    • Indie Hackers community
  3. 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

  1. Investigar términos de API ANTES de construir - Me salvó meses de trabajo
  2. Garmin vs Strava - Mejor decisión del proyecto
  3. GDPR desde día 1 - Evitar problemas futuros
  4. Métricas claras - Sé cuándo pivotar o parar

❌ Errores

  1. Empecé con Strava - Perdí 2 semanas antes del pivot
  2. Diseño inicial muy complejo - Debí empezar más simple
  3. 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.