Zum Inhalt springen
Glowing research cards whose light paths converge on the right into a central coach node — evidence becoming a coach, as a glowing low-poly illustration.
All posts
·AISpec-DrivenArchitectureLaravel

An app that doesn't guess

How I build coachframe.ai spec-first: concept before code, an evidence base of real studies, and a deterministic fact layer in front of the LLM. With Laravel, Nuxt and React Native as the output.

coachframe.ai is a SaaS for science-based multisport coaching. Athletes train the way a chosen coach persona would — backed by sources, in a chat. This post isn't about the sport. I'm interested in the engine room.

Most training apps guess. They throw a formula at your heart rate and call it science. I wanted the opposite: every number the coach produces can be traced back to a real primary source — or is honestly flagged as derived. No silent value without provenance. That single requirement shapes how the whole project is built.

Specification first, code second

The project runs spec-first. Before a line of code exists, the concept is written. I use specumentation for that, my own convention for concept → epic → ticket → code. The directories are fixed: docs/concept/ for the specification, docs/epics/ for the tickets.

Where it stands today: 24 numbered concept documents, 13 epics, 99 tickets. Each concept carries a status — draft, approved, implemented — and links cleanly to the others. An epic is derived from one or more concepts, and each ticket points at the specific concept paragraph it hangs on:

### [E-13.04] Multisport distribution in the generator
Status: done
Concept ref: [24] §1, [02] §3

The real payoff is the feedback loop. An audit of a concept reveals that an area already marked "done" actually had a gap — at which point a new concept and a new epic reopen the old one. The specification leads, the code follows. Anyone who has tried to reconstruct a concept backwards from grown code knows why this is the calmer option.

specumentation on GitHub

The spec-first convention coachframe.ai is built on: concept → epic → ticket → code, with status and cross-references.

The evidence base: guess nothing, source everything

Next to the apps, at repo level, sits a research/ directory. That's the source base for the coaching LLM. 112 study cards across six domains — training, load/recovery, psychology, nutrition, gender/age, strength. Each card is a real, peer-reviewed primary source, not a blog post and not an AI summary of unknown origin. The full source list is public on coachframe.ai/wissenschaft.

A card isn't just a citation. It has YAML frontmatter with DOI, evidence class, sample size and population, plus a validation block recording who checked the source against PubMed and when. The important part is the machine-readable section: each card emits decision rules that feed the plan generator.

decision_rules:
  - when: "easy_run AND hr > aerobic_threshold"
    then: "flag intensity drift"
    rationale_study: "2010-seiler-best-practice-distribution"

To make that hold up in the product, the sourcing requirement itself became a concept. Every dosimetric value in the generator carries either rationale: [<slug>] — a reference to a real card — or derived: [<field>], when it's derived from sourced quantities. A build test validates the slugs against the cards that actually exist. An unsourced value fails CI.

From study to coach persona

Turning the cards into a coach that holds a position in chat takes two steps: aggregation and personification.

Aggregation. A single study isn't a training rule yet. So every card carries a machine-readable block of when / then rules and the finding that backs each one:

- when: athlete.training_status in ["well_trained", "elite"]
        and athlete.tid.hit_share_by_session > 0.30
  then: coach.warn("High-intensity share is high — no long-term benefit shown, check recovery risk.")
  rationale_study: 2010-seiler-best-practice-distribution

The rules from many cards are condensed, per training school, into a method doctrine — polarized (Seiler), VDOT (Daniels), block periodization (Rønnestad), the Norwegian double-threshold (Tjelta-Bakken) and four more. Eight doctrines, each tied to real research and a real training philosophy. From those a machine-readable rule base is built, which the plan generator draws on.

Conflicts are part of it. Studies disagree. Seiler says polarized, a newer paper shows the base phase running more pyramidal. That's resolved not by a coin toss but by phase: pyramidal in the base, polarized toward the peak. The contradicting card isn't deleted, it's superseded — it stays readable, and the recommendation gets more cautious at that point. Often a card that contradicts one school isn't a contradiction at all, just the evidence for a different school.

Personification. The eight methods are the internal deep layer — the athlete never sees them directly. On top sit 85 coach characters with a name and a face, each tied firmly to one of the eight methods. Instead of "you now train by school X", the athlete picks a coach who brings an attitude.

The key is the split between method and style. The method sets the content — what's in the plan. The style sets only the language — how the coach says it. There are six styles, from plain-factual to socratic, and each method only permits the styles that fit it. A sober, scientific polarized coach in a drill-sergeant tone doesn't exist — it would be a character that contradicts its own method. The split has to stay strict; blur it in the code and the coach's voice frays.

Claude as a tool in several places

Pulling a project like this off alone — research, concept, three apps — only works with leverage. Claude is one of those levers, at several points in the chain:

  • Research. A PubMed fetcher (NCBI E-utils) pulls the records, Claude reads abstracts, classifies them and fills in the card to the template. The numbers come verbatim from the abstract, and the validation note records where the full text is still missing.
  • Aggregation. Claude condenses the cards into method doctrines and resolves contradictions between studies cleanly.
  • Conception. The concept documents and epics emerge in dialogue — I set the direction and the audit eye, the model keeps structure and cross-references consistent.
  • Implementation. Laravel services, Nuxt components, the Expo app. The output is ordinary code, not a black-box generation.

Claude isn't an end in itself here, and it isn't magic — it's one tool among several that saves time in the right places. The audit eye, what's right and what isn't, stays with me.

The output: three apps, one shared core

The monorepo has three apps and shares knowledge and sources at repo level.

  • apps/api — Laravel 13, PHP 8.3, Sanctum. Postgres, Redis, S3.
  • apps/web — Nuxt 4, Vue 3. No Tailwind, but a custom token system called "Night Ride".
  • apps/mobile — Expo, React Native. Reads Apple HealthKit and ships workouts plus daily vitals to the API.

Data comes from Garmin and Apple Health. Because the same session often arrives via both paths, a deduplication step runs before analysis — otherwise one run counts twice toward training load.

The chat: facts first, model second

This is where I step off the expected path. The obvious route for a coaching chat would be an agent with tool calling: the model invokes functions itself to read the plan or fetch activities. That's prepared in the SDK wrapper.

It isn't used in the active path. Instead, a context assembler gathers the facts deterministically, before the model is involved at all: the session's analysis markers, current form, the performance anchors. A persona builder turns that into the system prompt — method doctrine, persona voice, guardrails, then the fact block. Only then does the model stream the answer.

Why not let the model fetch the facts? Because the product promises not to guess. A deterministic fact layer is verifiable and reproducible. Tool calling is powerful, but it moves control to where I least want it for a source-backed product. The architecture is prepared in case that changes.

The coaching LLM is Claude throughout; the model is chosen per purpose — Sonnet for the chat, Opus for planning and review, Haiku for short notices. Every request lands in its own table with token counts and cost, so nothing runs in the fog.

What's left

Spec-first costs time up front and pays it back at the end. The evidence base is work you see the moment someone asks "why, exactly?" — then there's a study sitting there, not an excuse. And a deterministic fact layer in front of the LLM is more boring than an agent with tool calling, but it does what it's supposed to.

That's all it is. For an app that's not allowed to guess, that's enough.

Sciencecoachframe.ai

The full source list and more on the approach behind the coaching.