DEV Community

Cover image for Building a Ride Analysis Web App with Antigravity and the Strava API
Akira Kikusato for Google Developer Experts

Posted on • Originally published at zenn.dev

Building a Ride Analysis Web App with Antigravity and the Strava API

Introduction

I ride bikes a lot these days, and like many cyclists I use Strava to log my rides. Even on the free tier it automatically syncs with smartwatches, cycling computers, and smart trainers, which is plenty for just keeping a record. But the more I learned about training, the more I wanted richer analytics and planning — and most of that lives behind Strava's paid subscription.

While poking around, I noticed Strava offers a public Strava API. That was enough motivation to build my own analytics on top of it.

Concretely, I wanted these features:

  • Ride and training management
    • Review past rides … ①
    • Visualize power and heart-rate distributions by intensity zone … ②
    • View power and heart-rate traces color-coded by intensity zone … ③
    • Show recent training load and fatigue to get a sense of current condition … ④
  • Ride and training planning
    • Build training plans by intensity and goal for upcoming events … ⑤
    • Recommend the next workout based on recent condition (training load and fatigue) … ⑥

I built the app using Antigravity. There are two parts: the analytics app itself, and an agent that suggests the next workout based on recent condition. This post covers the analytics app. The agent will be a separate post.

Overview of the App: "PerfRide"

The result is a web app called PerfRide that uses the Strava API to manage ride records and training plans for road cycling.

GitHub logo kikuriyou / PerfRide

A performance management toolkit for road cyclists

PerfRide 🚴

A performance management toolkit for road cyclists, powered by the Strava API.

Simulate climbs, optimize race pacing, plan periodized training, and track your fitness — all in one app.

Features

📊 Dashboard

Connect with Strava to view your recent rides, weekly training summary, and fitness progress chart (CTL / ATL / TSB). Includes per-ride detail with heart rate zones, power profile, and elevation overlay.

🏔️ Climb Simulator

Predict climbing times based on power, weight, and real segment data. Uses physics-based simulation (air resistance, rolling resistance, drivetrain loss). Search segments by map or use your Strava starred segments.

🎯 Pace Optimizer

Calculate optimal pacing strategy for time trials based on course elevation profile. Based on the research paper "A numerical design methodology for optimal pacing strategy in the individual time trial discipline of cycling" (Sports Engineering, 2025).

📅 Training Planner

Generate periodized training plans for your target race…

The main features (including some experimental ones) are:

Feature Description Maps to Needs Strava Experimental
Dashboard Activity list, Fitness/Fatigue/Form charts ①, ②, ③, ④ Yes
Weekly Plan This week's workout plan ⑤, ⑥ Partial
Climb Simulator Predicts climb time from power and weight using physics-based math Partial
Pace Optimizer Computes optimal pacing based on a course profile Partial
Training Planner Generates a phased training plan working backward from a goal race No
Settings User parameters: FTP, weight, max HR, etc. No

PerfRide landing page
PerfRide landing page

Dashboard

Once connected to Strava, the dashboard shows your recent rides and a weekly summary (ride count, distance, elevation gain, moving time). It also derives fitness metrics from roughly the last 13 weeks of activity data and visualizes the following three indicators along with trends in distance and elevation:

  • Fitness (CTL: Chronic Training Load): Long-term accumulated training load. Higher = better aerobic base.
  • Fatigue (ATL: Acute Training Load): Fatigue from recent training. Higher = more tired.
  • Form (TSB: Training Stress Balance): Fitness − Fatigue. Indicates how race-ready you are (+10 to +25 is generally considered optimal for racing).

Clicking on a ride drills into details like heart-rate zone distribution, power profile, and elevation chart.

Dashboard page
Dashboard page

Weekly Plan

The Weekly Plan shows "what training you should do this week" laid out day by day. The Dashboard surfaces a card for just today's session, and the Weekly Plan page shows the full week (session type, target TSS, status) along with past plan history.

Weekly plan page
Weekly plan page

The plan itself is generated by a separate agent that runs early every Monday morning and builds out the following week automatically. It looks at recent Fitness/Fatigue/Form and user settings, then proposes sessions aligned with a periodization cycle (Base → Build → Peak → Taper). I'll cover the agent in a separate post.

About the experimental features

The three features below — Climb Simulator, Pace Optimizer, Training Planner — are currently marked as experimental. They work, but the UX flow and parameters are still rough. I built them quickly with help from Antigravity and LLMs, referenced some papers, but haven't fully polished them yet.

Climb Simulator estimates climb time. Enter power, weight, distance, and elevation gain, and it computes a predicted time using physics-based math. If you're connected to Strava, you can also estimate times for your starred segments.

Climb Simulator page
Climb Simulator page

Pace Optimizer computes optimal power distribution based on a course profile. It's based on a 2025 paper, "A numerical design methodology for optimal pacing strategy in the individual time trial discipline of cycling". The core idea: push higher power on climbs and lower power on descents to shorten total time while keeping Normalized Power (NP) roughly constant. Intuitively, at lower speeds on climbs, aerodynamic drag is smaller, so extra watts translate more directly into time saved. There's still room to improve, but it's a feature grounded in recent research.

Pace Optimizer page
Pace Optimizer page

Training Planner generates a phased training plan working backward from a goal race date. The phases are split roughly like this:

Phase Share of time Purpose
Base 35% Build aerobic foundation
Build 1 25% Tempo and threshold work
Build 2 20% VO2max and high-intensity
Peak 10% Race-specific simulation
Taper Remainder Recovery and tuning

Training Planner page
Training Planner page

Architecture

System overview

The app is structured as frontend + BFF + external APIs, with the next-workout-suggestion agent split off as a separate service.

The frontend doesn't call the Strava API directly. Instead it goes through Next.js API Routes (BFF pattern), which lets me keep access tokens, refresh logic, and a cache layer on the server. The browser never sees secrets, and rate-limit-friendly caching only needs to live in one place.

There's no database for ride history — Strava is the source of truth. Activity lists, ride details, and segment info are fetched from the Strava API on demand and cached for a short window inside the API Routes (BFF layer). That removes the need for a sync job and doubles as rate-limit protection. The only data I persist app-side is per-user settings, tokens, and weekly plans, all stored in Cloud Storage. No schema migrations, no per-environment ops; for personal-scale use this setup is plenty.

The agent that suggests and adjusts the next workout combines "LLM + domain logic," and Python has a stronger library ecosystem for that (and I'm more comfortable in Python for ML work), so I split it out as its own FastAPI service on Cloud Run. Details in a future post.

┌─────────────────────────────────────────────────────┐
│                    Frontend                          │
│  Next.js 16 (App Router) + TypeScript + React 19    │
│  - Auth: NextAuth.js                                 │
│  - Charts: Recharts                                  │
│  - Maps: Leaflet + React-Leaflet                     │
└─────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────┐
│               API Routes (BFF)                       │
│  Next.js API Routes (server-side)                    │
│  - Strava access token management / refresh          │
│  - Cloud Storage read/write (settings/cache/plans)   │
└─────────────────────────────────────────────────────┘
            │                          │
            ▼                          ▼
┌─────────────────────────────┐  ┌─────────────────────────┐
│     External Services        │  │        Storage          │
│  - Strava API (OAuth 2.0)    │  │  - Cloud Storage        │
│  - Nominatim (geocoding)     │  │   (JSON objects only)   │
└─────────────────────────────┘  └─────────────────────────┘
            │
            ▼
┌─────────────────────────────────────────────────────┐
│   Agent Service (covered in a future post)           │
│  Python / FastAPI / Google ADK + Gemini             │
│  - Daily Recommendation                              │
│  - Weekly Plan Generation                            │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The main tech stack:

Category Stack
Frontend Next.js 16 (App Router) / TypeScript / React 19
Visualization Recharts (charts) / Leaflet + React-Leaflet (maps)
Auth NextAuth.js (Strava OAuth 2.0)
Agent Python / FastAPI / Google ADK + Gemini (covered in a future post)
Storage Cloud Storage (JSON objects only; no relational DB)
Deployment Cloud Run (Web and Agent deployed as separate services)

Handling Strava API rate limits

According to Strava's official docs, as of May 2026 the default rate limits are as follows, applied per application. Going over returns 429 Too Many Requests.

Category 15 min 1 day
Overall 200 req 2,000 req
Non-Upload (read-heavy) 100 req 1,000 req

The request count limits weren't really a problem in practice. The bigger constraint is Connected Athletes = 1. With that default, only one user (me) can authorize the app, so I can't easily share it with friends. To raise it, you submit a form via Strava's Developer Program — they approved my request and bumped the limit within a day or two.

Notes on development

In the end the split landed at: frontend and BFF (Next.js API Routes) in TypeScript, and the agent (LLM + domain logic) in Python (FastAPI). I write Python day-to-day at work and don't have deep TypeScript experience, but writing the web side in TypeScript wasn't really a problem this time. Two main reasons (with the caveat that this app is small):

  1. You can grok the logic by talking to the LLM. Asking "explain the logic of this chart" or "show me where this is implemented" gets me the shape of the logic and the right code to read.
  2. Tweaking physics constants and formulas is mostly language-agnostic. Editing constants like const GRAVITY = 9.81 or basic arithmetic and trig formulas doesn't really depend on the language — as long as you can find the right spot. Iterative optimization code is a different story; for that you do need to understand the implementation itself.

On the other hand, the agent (next-workout suggestion and adjustment, covered separately) leans on the Python LLM ecosystem and my own familiarity, so I kept it separate. I did briefly experiment with using Python for the web backend, but the integration with the frontend created more debugging overhead, so I settled on "TypeScript for the UI-adjacent layer, Python for the LLM/inference layer." Carefully written interface definitions might mitigate that friction. Either way, the right split depends on the app's size and the kind of logic involved, and I'll keep refining my heuristic here.

Source code

The source code is available here. The README walks through how to set it up yourself. It runs locally, and I've also made it deployable to Cloud Run so I can access it on the go from a phone. Use the deploy script (deploy.sh.example) as a starting point and plug in your own project ID.

GitHub logo kikuriyou / PerfRide

A performance management toolkit for road cyclists

PerfRide 🚴

A performance management toolkit for road cyclists, powered by the Strava API.

Simulate climbs, optimize race pacing, plan periodized training, and track your fitness — all in one app.

Features

📊 Dashboard

Connect with Strava to view your recent rides, weekly training summary, and fitness progress chart (CTL / ATL / TSB). Includes per-ride detail with heart rate zones, power profile, and elevation overlay.

🏔️ Climb Simulator

Predict climbing times based on power, weight, and real segment data. Uses physics-based simulation (air resistance, rolling resistance, drivetrain loss). Search segments by map or use your Strava starred segments.

🎯 Pace Optimizer

Calculate optimal pacing strategy for time trials based on course elevation profile. Based on the research paper "A numerical design methodology for optimal pacing strategy in the individual time trial discipline of cycling" (Sports Engineering, 2025).

📅 Training Planner

Generate periodized training plans for your target race…




Wrap-up

This post introduced PerfRide, a minimal app built on top of the Strava API. With agentic coding tools like Antigravity, even outside your home language and library ecosystem, you can put together analytics that fit your own needs without much friction. As a bonus, it can save on subscription costs for paid features. For personal projects, I think it's a strong approach — give it a try.

I'll write up the agent features ("next recommended workout" and "automated weekly plan generation") in a separate post.


This article was originally published in Japanese on Zenn.

Top comments (0)