Blog

How we built Navara

July 16, 2026

The first commit landed on February 22, 2026. Five months, 505 commits, and 161 pull requests later, the public beta is live: an iOS app, an optional sync backend, an AI coaching connector, a browser-based Strava importer, and this website. The team is one runner who decides, and Claude Code, which builds. We've already written why Navara exists; this is the story of how it got built.

The architecture: local-first, honestly

The iPhone is the source of truth. Workouts flow in from HealthKit observers into SwiftData, and everything you see — records, streaks, plans, the heatmap — is computed on-device. The Supabase backend is a projection of your device, not the other way round: sync is optional, off by default, and the app is fully functional without an account.

That stance bought us real things. Your training can be donated to Apple Intelligence's on-device index without a byte leaving the phone. Plans recalibrate the moment a run imports, not when a nightly job notices. And the privacy page is short because there isn't much to confess.

It also charged us for it. When the device is the source of truth and the cloud is a mirror, every sync path is a data-integrity feature, and every parser is load-bearing. Which brings us to the war stories.

Four bugs that earned their scars

  • The date that ate a training plan. Swift's ISO8601 parser, configured for fractional seconds, silently rejects timestamps without fractions — which is exactly what Postgres emits on the stroke of a whole second. Our fallback was ?? Date(), so twenty-one weeks of plan dates quietly collapsed to "now". Lesson: a fallback that fabricates plausible data is worse than a crash. The parser now tries three formats and the merge refuses to overwrite a real date with a sentinel.
  • The invisible 1,000-row ceiling. PostgREST caps unfiltered queries at 1,000 rows by default. Our dedupe check fetched "all existing activities", got exactly 1,000 of 1,594, and cheerfully imported 700 duplicates. Pagination is not an optimisation; below the first page limit is where correctness lives.
  • Fourteen years, zero matches. Strava's CSV export writes UTC timestamps with no timezone marker, so JavaScript parsed them as local time and every single one of 1,594 activities missed its Apple Health twin by exactly eight hours. One regex later, 1,482 matched.
  • The 404-degree map. The all-time heatmap worked beautifully until it spanned the UK and California at once, at which point MapKit threw an exception for a region 404 degrees wide. Clamp your spans; the world is only 360 degrees around.

An AI coach with a hand on the wheel

The most unusual piece is the coaching connector: Navara runs an MCP server (OAuth 2.1, at navara.fit/mcp) so you can talk to your training plan from Claude. The AI never edits your plan directly — it submits a changeset, and every operation runs through a safety validator implemented twice, in lockstep: once in TypeScript on the server, once in Swift on your phone. Two codebases, one rulebook, so neither side can be talked past.

The validator itself has a story — we watched the coach make genuinely good recommendations that our own guardrails blocked, and ended up rebuilding the rules as warnings instead of walls. The training engine got smarter because the AI argued with it.

The importer runs in your browser

The Strava importer never sees your raw archive on our servers. Your export ZIP is parsed entirely client-side — GPX, TCX, and binary FIT files, gzipped or not, decompressed with the browser's own DecompressionStream — deduplicated against what you already have, and only the structured results sync up. The test suite was my own fourteen-year archive: 1,594 activities, three file formats, one leading-whitespace bug in Strava's TCX files that took an evening to find.

What building with an AI is actually like

The loop that built Navara looks like this: I notice something on a run or in a screenshot — "the proposal sheet is ugly and it's in kilometres, I run in miles" — and describe it in plain English. Claude Code reads the codebase, finds the cause, writes the fix, runs the build, and opens a PR. I review, test on device, and decide. The 161 pull requests span Swift, TypeScript, SQL, and this very page.

The honest division of labour: the AI is tireless at breadth — tracing a date bug across an iOS parser, a Postgres schema, and a sync layer in one sitting. The human owns taste and stakes: what ships, what a coach should never be allowed to do to a marathon taper, when "done" is actually done. Neither half builds this alone in five months.

The beta is open to everyone, free during the beta. Come find the bugs we haven't written war stories about yet.

Stop training blind.

See the progress you've been missing. Free to get started — connect Apple Health and go in minutes.

← All posts