Teissier YannisDeveloper · AI & fullstackLyon · Sept. 2026
Back to the indexProject 03 of 06
03Case study — 2026

AltiTrak
trail-running analytics where elevation comes first

GPX and Strava activities turned into vertical speed, climb splits, run/hike ratio, training load and a "you vs the race" readiness score. FastAPI, PostGIS and TimescaleDB, React.

Role
Product, backend, data model, frontend, CI
Stack
FastAPI · PostgreSQL · PostGIS · TimescaleDB · React · Playwright
Status
Shipped through V2, then archived; a microservices rewrite followed

The problem

Running apps are built for the road: pace per kilometre, weekly distance. On a trail, pace is meaningless and the climbing is the workout. I wanted an app that reads an activity the way a trail runner does: how much did I climb, how fast did I go up, how much of it did I walk, how hard was the descent, and that can answer one question before a race: am I ready for this one?

What I built

A monorepo with a React front end and a FastAPI back end in four layers, where the domain is pure functions with zero external imports: data quality flags, climb segmentation, vertical speed up and down, time per 100 m, run/hike split, a "destructive descent" index. Track points live in a TimescaleDB hypertable with a PostGIS geometry column, one instance for both time series and geography, a decision recorded as an ADR, chosen over a second database.

On top of it: a training load that weights each session by its elevation (D+ and D− count differently), a double performance-management chart (global with cycling as cross-training, trail-only without it), weekly alerts, and the "you vs the race" score: a race's distance and elevation against the last four weeks of volume, climbing, long runs and descent exposure, returned as a 0–100 readiness with three strengths, three gaps and three actions. An MCP server runs inside the same process so an assistant can query the data. CI runs lint, audits, unit tests and about two hundred Playwright end-to-end tests, smoke on every deployment and the full suite nightly.

fig. 1 — no screenshot, a drawing
Fig. 1 — from a track file to a readiness score · and the climbs the app actually cares about01

What I learned

That the first version of the load model overestimated fatigue two to four times (a missing warm-up window, a terrain factor applied to the wrong input), and that comparing against an established tool is how you find out. That an app should work with only a type, a date and a duration, and treat every other field as optional. And that a monolith with pure domain functions is a fine place to be; the microservices rewrite that followed taught me the cost of the alternative.

“A trail runner's week is measured in metres climbed, not in minutes per kilometre.”
Next — 04A modded Minecraft server, in production