Developing an Obsidian Plugin for Garmin Connect
An open-source Obsidian plugin that syncs Garmin Connect metrics to your vault and visualizes them on desktop and mobile.
For those of you who don't know, Obsidian is a note-taking app built on plain Markdown files. One of the neat quirks of the app, and part of its core functionality, is that you can link notes to each other and visualize your digital brain growing, so to speak — a collection of everything you've recorded, from study notes and thoughts to daily entries.
But plugins are where the real magic happens. Any non-native functionality can be developed and added to your vault, so the possibilities become pretty much limitless. Needless to say, if you haven't tried the app, I highly recommend you do! It might be a little daunting at first, but stick with it and it becomes really fun and rewarding.
Back to the main topic at hand. Garmin Connect is used to connect to Garmin wearables and store the data they measure: sleep, heart rate, steps, workouts, and a lot more. It's great in the sense that pretty much all the health data you could ever need is stored there. You can view it through Garmin Connect on mobile and the web, and Garmin provides a few ways to export it, but there isn't a nice personal-use API that lets you freely query all of it.
Wouldn't it be great if you could store, visualize, and query that data in Obsidian...
Well, fear not, we built a plugin that brings the two together. Garmin authentication is seamlessly integrated into your vault, each day's metrics are written into your daily notes as properties. Those properties can be organized in a queryable Bases table, and the data can be visualized through an interactive dashboard directly in your vault.
Last but not least, it's compatible with both mobile and desktop.
The rest of this post describes the major pitfalls we had to overcome and the end result.
Garmin API
Garmin does have an official Connect Developer Program, but it's explicitly intended for business use. For an individual who just wants to pull their own step count into a side project, there isn't a self-serve API key, OAuth client, or documented consumer endpoint.
Fortunately, there was an actively maintained repo, cyberjunky/python-garminconnect, which made the process so much easier.
It gave us two very important things:
- 01It validated that headless authentication was possible.
- 02It mapped out Garmin's non-public endpoints and HTTP request / response structures.
All we needed to do was translate the relevant logic into TypeScript while avoiding Node.js or Electron APIs that would make the plugin incompatible with mobile.
For all my non-technical folks reading this, please do not fret over the seemingly gibberish words. Truth be told, we software engineers don't like words or reading either. After all, it's 2026 — if it's not short-form content with Subway Surfers taking up the bottom half, is it even real content?
Unfortunately, I'm all out of Subway Surfers footage, but here's a diagram I made instead. This format of illustration is called a Sequence Diagram — SD if ya nasty.
We had a bit of a scare right off the bat. Garmin's sign-in flow has bot-protection measures in front of it, and python-garminconnect has gone pretty far to work around false positives. Some of its authentication strategies impersonate browser or mobile TLS fingerprints, and some of its web-login paths deliberately pause before submitting a login.
Inside Obsidian, you get requestUrl, and you don't get the same low-level control over the network stack.
So before writing anything else, we tested whether authentication would work without trying to imitate one of those browser TLS fingerprints. We tested this on desktop and iOS, and authentication was successful on both.
That doesn't mean Garmin ignores TLS fingerprinting everywhere. What it does mean is that, in our testing, the mobile sign-in path worked through Obsidian without needing that extra impersonation layer.
Great news for us.
Garmin could tighten this restriction eventually, but for now, we're good.
Mobile Compatibility
Obsidian on mobile runs in a WebView-based environment. This is a bit of an issue since Node.js and Electron APIs available for desktop plugins, are incompatible for mobile.
Garmin Health Sync is another Obsidian Garmin plugin and something I drew a lot of inspiration from. Initially, I forked the repo and started building on top of it, ready to send PRs with the dashboard view and other features I had built.
Eventually, though, my version started to diverge too much. I wanted headless authentication, mobile compatibility, and a fairly opinionated dashboard, so it made more sense to build my own plugin from the ground up.
One limitation of Garmin Health Sync for my use case is that authentication uses an Electron browser window, which makes that flow desktop-only.
As discussed earlier, we found another route.
For the network layer, the main piece was using Obsidian's requestUrl. It's similar to the standard fetch() API, but Obsidian provides it without normal browser CORS restrictions, and it works across Obsidian's supported platforms.
Visualizing the Numbers
So now we can authenticate, and pulling data is rather simple.
Unfortunately, a wall of numbers in a base file doesn't help much.
Okay, let's take a step back. Here's what's going on.
First, metrics land as note properties, so Dataview and Bases can query them. Each sync walks backwards day by day and only updates a note when something has actually changed. We also have to be careful about how aggressively we hit Garmin's endpoints so we don't get rate-limited.
Second, a dashboard.
Interactive Dashboard
The UI uses Svelte. We had the choice between that, native TypeScript, or React, and I ended up going with Svelte because its compiled approach fit what I wanted from a lightweight Obsidian plugin.
React goes through a render and reconciliation step before committing the necessary changes to the DOM, often described in terms of a virtual DOM. Svelte takes a different approach: the compiler can generate targeted updates to the actual DOM.
That doesn't magically make Svelte faster in every situation, but for a small plugin where bundle size and UI overhead matter, I liked the trade-off.
The charts are made using SVG's. I also chose not to pull in a full Svelte component library because I wanted to keep the plugin bundle small. Instead, I can use existing libraries as design references and hand-roll the components I actually need.
What's next
The plugin is not perfect, and never will be. The life of a developer and active maintainer is much like Sisyphus and his endless task of rolling the boulder up the mountain.
But every day we go back to rolling that boulder and try to take another step forward.
Here are some things we plan to add:
- Handle MFA accounts
- Improve dashboard UI
- Add CI/CD pipelines to autodetect if anything breaks or Garmin changes its API
- Create custom components that you can embed directly in your Markdown files
Acknowledgements
This project wouldn't have been possible without the developer community. I want to properly acknowledge the openly available projects and resources that helped me get here:
- Garmin Health Sync, for the idea and for showing how far file properties and daily notes can go. I wanted headless sign-in, cross-platform compatibility, and a dashboard, but the original inspiration started there.
- cyberjunky/python-garminconnect, an actively maintained Garmin Connect library that saved us a huge amount of reverse-engineering work.
- health-md, which I plan to use as an inspiration around health-data UI and visualization components.
- The Obsidian community, for the support and the beta testing.

