Skip to content
Xenia Kadar
← Back to work

Personal running intelligence

StrideSense

I was paying for several subscriptions to see my own health data. So I built the app I actually wanted.

StrideSense combines your running, recovery, weather and glucose history to explain why a run felt different - not just what the metrics were.

FastAPI · PostgreSQL · pgvector · Next.js · TypeScript

StrideSense run detail for a 17.5 km long run in New York: time, pace, heart rate, and temperature up top; an Insight card attributing the harder-than-planned effort to temperatures climbing from 22.6°C to an apparent 27.3°C; and a glucose chart showing 100% time in range.

Overview

What it does

StrideSense combines your running history with weather, recovery, and glucose data, then compares each run with similar runs from your own past to explain what was different, why it may have felt that way, and what to do next.

How it stays trustworthy

Deterministic code performs every calculation. The Claude API only translates those computed facts into plain language; it cannot generate or alter the underlying data.

Role
Product, design, and engineering
Stack
FastAPI, PostgreSQL + pgvector, Redis, Next.js, TypeScript
Data sources
Apple Health, Oura, Open-Meteo
Status
MVP done, development in progress

Why I built this

I love wearables. Over the years, I’ve owned Apple Watches, two generations of Oura rings, a Garmin, a WHOOP, even a smart blood pressure monitor and a CGM (despite there being nothing wrong with my blood sugar).

But all that data lived across different apps and paid subscriptions. I was paying Oura for sleep data, Garmin Connect+ for deeper performance insights, and Strava for the useful training analysis. During marathon training, one run could feel unexpectedly hard when the previous day’s had felt fine, and the apps would show me the symptoms - heart rate up, pace down - but not the cause. Was it poor sleep? Humidity? Accumulated training load?

The data existed on my devices. The connections between it didn’t exist anywhere.

I wanted one place where I could see the full picture and actually draw conclusions from it. With export APIs, deterministic analysis, and an LLM to handle the narration, I realised I could build the connected version myself.

  • Apple Watch

    Heart rate · workouts

  • Oura ring ×2

    Sleep · HRV · temp

  • Garmin

    GPS · pace · cadence

  • WHOOP

    Strain · recovery

  • Blood pressure

    Systolic · diastolic

  • Glucose sensor

    Interstitial glucose

Six devices. Five apps. One shared archive — zero shared conclusions.

Built around one question

Every feature in StrideSense answers the same question: why did this run feel the way it did?Anything that didn’t help answer it, like leaderboards, badges, app-specific readiness and recovery scores, stayed out.

Three rules kept the scope honest:

  • Code computes. Claude narrates.

    Similarity, training load, and weather context are calculated deterministically — Claude's only job is to explain numbers that already exist.

    Every insight cites the specific runs it reasons from.

  • Import a history. Don't manufacture one.

    The app ingests years of existing data instead of asking you to log runs for a month before it has anything to say.

    Already useful in the first five minutes.

  • When evidence is weak, say less.

    A missing value displays as missing, and a run type that can't be confidently inferred stays unlabeled.

    The classifier marks 8% of my history, not 80%.

From sketches to product

Six hand-drawn phone wireframes: home, recent runs, trends, ask, and two cities views — a world map and a list.
Early sketches for Home, Runs, Trends, Coach, and Cities. The visual design evolved; the information architecture remained.

The core structure was defined before I wrote any code: Home, Runs, Trends, Coach, and the run detail page where most of the product lives. The interface changed, but the information architecture survived from paper to production.

I built it in five phases, each with a clear exit:

  1. Schema and local stack
  2. Manual logging and dashboard
  3. Analytics engine
  4. Apple Health import
  5. Grounded insight layer

The analytics engine — run similarity, training load, and grounded insights — was protected from scope changes. New ideas went into the backlog, not the current phase.

The roadmap changed during development. Strava was originally the import path, but its API became paid mid-project. I switched to Apple Health, which turned out to be the better source because it contained my full running history across every device I had used.

Real data also changed what the product needed to capture. A long summer run could start at 17°C and finish at 26°C, yet most running apps reduce the weather to one number. StrideSense stores conditions across the run — including start and end temperature, apparent maximum, humidity, and wind — because “it got hot halfway through” is often the reason a run felt hard.

  • StrideSense home screen: 43 km across 4 runs this week, a suggestion to take an easy run or rest with training load marked optimal at ACWR 1.0, a 12-week weekly-distance chart, and a daily overview noting below-average sleep.
    The shipped home screen — weekly load at a glance, today's suggestion pinned to a computed load state (ACWR 1.0, optimal), and a daily overview that folds last night's sleep into the day's call.
  • StrideSense trends screen: city chips for New York, Lisbon, and San Francisco, a 90-day training-load chart plotted against the optimal 0.8–1.3 zone, and a glucose time-in-range chart, with a Customize button.
    Trends is the longitudinal layer: training load plotted against its optimal zone and glucose time-in-range — the defaults answer “how am I doing lately?”
  • Cities map screen showing seven cities across five countries on a world map, with markers on San Francisco, New York, and Lisbon.
    The map is the same history, spatially — see everywhere you've run at a glance.

Three decisions that shaped the product

Core product decision

1. Compared to the right runs, not to averages.

“Slower than your average” means very little when today was a hot long run, a hilly route, or an easy recovery day. StrideSense compares each run with the five closest matches from your own history: similar distance, effort type, and conditions.

That makes every comparison answer a more useful question:

Was this good for this kind of run, for me?

Technical detail

Similarity is a weighted cosine over z-scored features: every metric is standardized against my own history first, so "similar pace" means similar for me. Distance and pace carry the most weight; weather is context, not a matching criterion. I computed the first three comparisons by hand before trusting the implementation.

Supporting trust decisions

2. The classifier that refused to classify.

My first run-type heuristic labelled a run “easy” when its pace and distance sat near my average. When I tested it against my full history, it classified 81% of all runs. The problem was simple: most runs sit somewhere near the average, so the label said almost nothing.

I removed it. The shipped classifier only assigns a type when the evidence is strong — such as long run, race, or recovery — and leaves the rest unlabelled.

3. Code computes, Claude narrates.

Claude never calculates anything in StrideSense. Python computes the pace deltas, training load, weather, and glucose statistics, then passes Claude a block of verified facts to explain in plain language.

Claude cannot introduce a number that is not already there. If the API is unavailable, the run page still shows the underlying facts instead of failing. When any input changes, the cached explanation is deleted and regenerated from the updated data.

Similar-runs screen comparing a long run against its five closest long runs: a pace-over-time chart with this run highlighted, delta tiles for pace, heart rate, temperature, and glucose, a verdict reading 'Right in line with your typical long run', and ranked matches from 84 to 90 percent.
Every delta here is measured against your five closest long runs — not your average, and not anyone else's.

Real data changed the product

The test set was nine years of my own runs. It found bugs no curated data would have.

The 138 bpm marathon

My marathon logged 138 bpm; the real figure was ~159. My watch and my ring were both writing heart rate, and the ring reads low during exercise — the app averaged truth with noise. The rule that came out of it: a run’s heart rate comes only from the device that recorded the run. No matching records, honest blank.

Conflict
Two devices reported different heart rates.
Rule introduced
Trust the device that recorded the run.

Six device eras, one history

The export held six generations of apps and watches — many runs recorded twice. Filtering to one source would delete history; keeping everything would double it. So: priority-based dedupe where a discarded duplicate donates what the survivor lacks (usually its GPS route). Every merge previewed as a dry run — which is how a false merge got caught in a review table instead of in my data.

Conflict
Duplicates across six device eras.
Rule introduced
Merge complementary fields; do not erase history.

One question audited nine years

Asking my history about races surfaced a “0°C half marathon” I remembered as warm — a GPS-less run wearing another city’s fallback weather. Years of dashboard views never exposed it; one question did.

Conflict
Missing GPS attached the wrong weather.
Rule introduced
Abstain when location evidence is incomplete.

Grounded AI

The same five-step workflow powers every AI feature in StrideSense, from individual run insights to the Coach.

  1. 01Compute

    Python calculates the pace deltas, training load, weather, and glucose statistics.

  2. 02Assemble

    Those results become a structured block of facts about the run and its closest matches.

  3. 03Narrate

    Claude translates the facts into plain language. It cannot introduce a metric or number that is not already present.

  4. 04Validate

    The response is checked against a typed schema before it is stored.

  5. 05Invalidate

    When an input changes — such as late-arriving weather, a corrected run type, or re-imported heart-rate data — the cached explanation is deleted and regenerated.

Claude receives computed facts and retrieved runs. It cannot invent metrics, and every answer cites the runs it used.

The Coach follows the same workflow. The only difference is retrieval: a sentence-embedding model identifies the runs most relevant to the question, then Claude answers using those runs as evidence.

Technical detail

Retrieval scores taught their own lesson: for "tell me about my races," the correct hits scored 0.41; weather questions scored 0.75. Absolute similarity means nothing across queries — only within-query ranking does. The pipeline ranks and never thresholds; a cutoff that keeps weather answers honest would silently delete race answers.

Coach screen answering 'How do I handle hot weather?' with a grounded summary of pace across hot New York runs, followed by 'Based on these runs' listing the five cited runs with dates and distances.
Answers cite the actual runs they're built from — dates, distances, conditions — listed under the answer. If a claim isn't in those runs, the model isn't allowed to make it.

What’s next

StrideSense isn’t finished yet. I’m constantly making small refinements, like pace characterized against my own distribution, hybrid SQL-plus-embedding retrieval, an evaluation set for retrieval quality. The bigger bets:

  • Improved pattern detection

    The Coach already notices patterns in the runs it retrieves, but a notice is an impression over five runs. The next layer turns impressions into measurements: deterministic analyzers test those relationships across the entire history and report effect sizes with sample counts, for instance: “you slow 9 s/km per °C above 22, across 41 runs” and below a minimum sample or effect, a finding doesn’t exist. The narrator then cites verified patterns instead of noticing them fresh: AI that reasons from measurements, not one that reads and riffs.

  • Research-grounded answers

    Published sports-science findings enter the context block the same way findings from your own history do with citable facts and sources attached, so an answer can connect your measured pattern to the studied one without inventing either.

  • Built for female physiology

    My previous work in women’s health involved translating research into symptom-tracking and health-assistant product requirements, so I’ve been close enough to the research to know that training guidance defaults to male physiology. Plans built and validated on men, applied to everyone. I want StrideSense to be able to judge effort against how a woman’s body actually responds.