Adding live training data to this website
March 11, 2026

I wanted a page on my website that showed my training data.
A heatmap like GitHub's contribution graph, but for workouts.
Why? I don't really know, but it looks cool and it's a fun way to view my training data.
And of course, I'm also not a fan of garmin connect's UI, so with this method I can view my data however I want.
The problem: Garmin has no public API
This is the main issue. You can't just grab a token and start fetching.
Luckily, there's there's python-garminconnect, a reverse-engineered Python library that wraps Garmin's internal APIs, so I just hosted that on my VPS.
Garmin uses OAuth tokens managed by the garth library under the hood. First setup requires MFA (I think only if you have ECG enabled), but after that tokens persist to disk and refresh automatically
After this boring part is done, I can easily query any any sort of data, like /sleep_data. /body_composition, etc.
Astro SSR on Cloudflare
I've built this entire site on Astro, just because I'm tired of next.js.
At this point, I've almost ditched it entirely and most of my projects run on React Router 7 or Astro.
I chose SSR over static builds because I'm not gonna be rebuilding the site every time I go for a run.
I use cf.cacheTtl so Cloudflare's edge caches the responses too. Combined with the VPS cache and browser Cache-Control headers, that's three layers of caching. Data is always fresh enough without hitting Garmin API's on every page load.
Withings Data
I use a Withings Body+ for weight, body fat, and muscle mass. It doesn't sync to Garmin natively, but withings-sync handles that.
All it does is pull data from the Withings API and pushes to Garmin Connect. It runs every day on a cron job on the same VPS.
Every morning after I weigh in, the data shows up in Garmin, and then my training page picks it up through the body composition endpoint. I show 30-day rolling averages instead of the last reading because daily weights can be noisy.
What I'd change
Honestly not much.
This whole thing only took 30m-1h to setup (thank you Claude for handling the boring setups) and then creating the frontend was also pretty easy.
My input was mostly the design and telling Claude what architecture to follow.
Stack
| Layer | Tech |
|---|---|
| Watch | Garmin Fenix 7 Pro |
| Scale | Withings Body+ > withings-sync > Garmin |
| API | Python FastAPI + python-garminconnect on VPS |
| Web | Astro SSR on Cloudflare Workers |
| UI | React 19 + Tailwind + Framer Motion |
| Cache | VPS (1h) > Cloudflare edge > browser |
Check it out at araujo.zip/training.