• 4 min read
A Homepage That Shows Its Owner’s Heartbeat
A developer wired Garmin watch data into a static site, replaying heart-rate readings from exactly 24 hours earlier.

Image: Hacker News
There is now a small red heart on Snehan Kekre’s homepage, and it beats at his actual recorded heart rate. If the number says 48 bpm, the icon pulses once every 1.25 seconds; at 80 bpm, it pulses every 0.75 seconds. What looks like a live status indicator is actually a carefully engineered replay of data from exactly 24 hours earlier.
Kekre built it using a Garmin Descent Mk3i, 43 mm, carbon gray DLC titanium, which he says he originally bought as a primary dive computer alongside a Shearwater Perdix 2. Since April 2025, the watch has gone on 400+ dives, including to 100 m. It also tracks heart rate, sleep, HRV, steps, calories, and workouts, syncing that data to Garmin Connect on a Pixel 9 Pro.
The goal was to turn that stream of personal biometric data into a more literal online-presence indicator than the green dots of older chat apps. Kekre argues that a heartbeat says more than whether a browser tab is connected: it signals that a person is physically there. He also says he understands the privacy tradeoffs, including the fact that heart-rate, sleep, and activity patterns can reveal travel, illness, stress, and when he is away from home.

Recommended reading
Avito eyes free dating inside its app
How Garmin data gets onto a static site
The obvious routes did not fit. Services like Pulsoid and HypeRate support Garmin watches, but they rely on broadcast heart-rate mode over BLE/ANT+, which Garmin warns can reduce battery life. That mattered because Kekre uses the watch for diving and did not want a homepage widget to compromise battery life on dive days.
Garmin’s official Health API also was not an option. It exposes the right data, but access is limited to business applications and review. Instead, Kekre used python-garminconnect, a Python client for the same private API used by the Garmin Connect app. He says it exposes essentially everything visible in the mobile app, including workouts, sleep, and HRV.
To avoid storing credentials in CI, he performs a one-time login locally. That produces:
- an OAuth1 token good for about a year
- an OAuth2 token that expires after a few hours and is refreshed automatically
Those tokens are stored as a base64 blob in a GitHub Actions secret. Kekre notes that Garmin rate-limited his IP with 429 errors during setup, but a fallback login strategy in the library eventually worked.
Why the widget replays yesterday
The site is built with Astro and served as static files from GitHub Pages, so there is no persistent backend to stream live data. More importantly, the watch does not continuously send heart-rate data to Garmin by default. Instead, it syncs in batches from the wrist to the phone and then to Garmin’s servers.
That forced a simpler design: fetch the previous day’s heart-rate data once in the morning, publish it as public/hr.json, and replay it on the client. At 14:32 in a visitor’s local time, the page shows Kekre’s heart rate from 14:32 yesterday, interpolated from real samples.
The fetch script runs on two daily cron schedules:
- *'0 19 '**
- *'0 4 '**
Those correspond to 03:00 in Bali, once the day is complete, and 12:00 as a fallback in case the phone sync happened late. Kekre had to explicitly set HR_TZ=Asia/Makassar because GitHub runners use UTC, which would otherwise pull the wrong day’s data.
The resulting JSON is about 14 KB and contains roughly 717 samples, about one every 2 minutes. Kekre notes that this is not the sensor’s real sampling rate: the watch measures multiple times per second, and Garmin’s official Health API offers 15 second representative samples, but the daily chart endpoint used here is aggregated to roughly 2-minute intervals.
On the client side, the browser simply evaluates:
Date.now() - 86_400_000
That gives the exact moment 24 hours earlier in epoch milliseconds, so visitors in Tokyo and New York see the same underlying point in time. The script then interpolates between nearby samples and treats gaps wider than 20 minutes as missing data. If Garmin is down, the watch has not synced, or token refresh fails, the fetch script exits cleanly and the widget disappears rather than breaking the build.
Kekre pins garminconnect==0.3.6 in CI because the project depends on an unofficial client for a private API, and he would rather a future break fail loudly than drift silently.
Computing Editor
Tomas lives in the terminal. He covers chips, laptops, and operating systems with a focus on performance and efficiency. He reads kernel changelogs the way other people read fiction, and he's always on the hunt for the perfect mechanical keyboard switch. If it processes data, Tomas has an opinion on it.
via Hacker News


