All projects
Mobile · Applied LLMSolo — product, mobile, backend, ML/2026

Onpace

A swim-coaching iOS app where AI writes training plans out of a knowledge graph — and is never allowed to overrule the coach.

Onpace is a swim training app on the iOS App Store: swimmers get structured plans and video technique review, coaches get a roster and an assistant that drafts work for them to edit. The interesting constraint is architectural — every generative feature is scoped so the model proposes and the coach disposes. Built and shipped solo, from the Cypher queries to the App Store listing.

v1.0.4
On the App Store
Health & Fitness — free, with an Onpace Pro subscription
−7 s
100 m pace, six weeks
1:45 → 1:38, swimmer-reported in a review
400 m
Beginner to continuous
from not managing a single length, swimmer-reported
Hybrid RAG
Plan generation
graph retrieval plus vector search over swim research

The product

A swimmer signs up, completes onboarding — level, goals, sessions per week, pool length, available equipment, weekly mileage, physical limitations — and waits on a plan. A coach sees them arrive in a roster, generates a draft plan, edits it, and assigns it. From there the swimmer gets a daily workout, logs RPE and mood on completion, tracks progress, and can send technique video for review.

Both sides of that are one Expo app with a role-aware navigator. Firestore is the single source of truth, subscribed through onSnapshot so a coach's edit appears on the swimmer's phone without a refresh, and scoped by role — coaches subscribe to their whole roster, swimmers only to their own documents.

Plans come out of a knowledge graph, not a blank prompt

Plan generation runs server-side in a callable Cloud Function, so the Anthropic key and the Neo4j credentials never reach a phone. The model is Claude Sonnet, but the interesting part is what it is given to work with.

Retrieval is hybrid. A Neo4j Aura swim knowledge graph supplies real drills — execution descriptions, coaching cues, required equipment, skill levels — matched to the athlete's actual constraints. On top of that, vector search over reference plans and swim-research passages, embedded with text-embedding-3-small at 1536 dimensions, pulls in relevant precedent. The model composes from retrieved material rather than inventing sets from its own priors.

The Cypher queries and prompts are shared verbatim with the uSport.ai web product, so a plan generated on the phone is identical to one generated on the web. The vector half degrades gracefully: without an embeddings key those two queries no-op and graph retrieval still carries the generation.

Giving the model a memory so it stops repeating itself

A language model generating a training plan has no idea what it produced for the same athlete last week. Left alone it converges — similar sets, similar distances, the same drills in a slightly different order. For a swimmer following a plan for months, that is the difference between coaching and a random set generator.

So each athlete carries a short rolling history in Firestore. The four most recent generated sessions go back into the prompt as explicit context, and the three newest are persisted for next time. The model can see its own output and is asked to move away from it. Small mechanism, and it is the difference between a plan that progresses and one that loops.

The model proposes, the coach disposes

Every generative surface in the app is deliberately bounded, because a swim plan is a physical prescription and a wrong one injures a shoulder.

AI-generated plans arrive as drafts, tagged as drafts, into a coach's editor — never straight to a swimmer. The in-app workout adjustment cannot author a workout at all: given 'I only have 30 minutes' it modifies the coach's existing session, preserving its focus, its plan, and its authorship, and it honors safety constraints — a swimmer reporting shoulder pain gets paddles dropped and intensity downgraded rather than a cheerful substitution.

The video review tool follows the same rule in a stronger form: it never grades the swimmer. The coach narrates while watching the clip, and the tool turns their own words into timestamped marks against a closed vocabulary of stroke faults — crossover, dropped elbow, hip drop, late breakout — each with an editable cue. The coach accepts or rewrites every mark. The app structures the coach's judgment; it does not substitute for it.

That last feature currently ships as a deterministic keyword-and-sentiment extractor rather than a live speech-to-text and LLM pipeline. It works offline, it is predictable, and the taxonomy it emits is the same one the model path will emit — so the upgrade is a swap, not a redesign.

Shipping it alone

Everything between the idea and the App Store listing was mine: the Expo client and its EAS builds, Firestore rules and indexes, roughly twenty Cloud Functions covering plan generation, PDF export, invites, email verification, and subscription webhooks, plus the App Store submission itself.

Billing goes through RevenueCat web checkout over Stripe, with two surfaces chosen automatically — the native SDK paywall in a real build, and a hosted paywall link as fallback where the native module cannot load. Authentication offers email and password, Google OAuth, and passwordless magic links.

The reviews are what a coaching product is actually judged on: one swimmer took a 100 m pace from 1:45 to 1:38 in six weeks; another went from being unable to complete a single length to swimming 400 m continuously.

Technical setup

Client
React Native / Expo 54, TypeScript, iOS
Data
Firestore with onSnapshot subscriptions, role-scoped
Generation
Callable Cloud Function, Claude Sonnet
Retrieval
Neo4j Aura swim graph + text-embedding-3-small (1536-d)
Billing
RevenueCat web checkout, Stripe-backed
Auth
Email/password, Google OAuth, passwordless magic link

System architecture

The client never sees a credential and never receives an unreviewed plan — generation and retrieval both live server-side.

Client — Expo / React Native

Swimmer
  • 6-step onboarding
  • Daily workout + RPE/mood log
  • Progress and streaks
  • Technique video upload
Coach
  • Roster and pending members
  • Plan editor with AI draft
  • Narrated video review
  • Inbox and session booking
Shared
  • Role-aware navigator
  • Firestore onSnapshot state
  • Role-scoped subscriptions
  • RevenueCat paywall

Generation — callable Cloud Function

Model
  • Claude Sonnet
  • Prompts shared with the web product
  • Draft output only, never assigned
  • Safety constraints honored
Retrieval
  • Neo4j swim knowledge graph
  • Drills matched to equipment & level
  • Vector search over research
  • text-embedding-3-small, 1536-d
Anti-repetition
  • Rolling per-athlete history
  • 4 recent sessions into the prompt
  • 3 newest persisted forward
  • Model sees its own prior output

Platform

Firebase
  • Auth: password, Google, magic link
  • Firestore rules + indexes
  • Storage for video clips
  • ~20 Cloud Functions
Commerce
  • RevenueCat web checkout
  • Stripe-backed subscriptions
  • Entitlement webhooks
  • Billing portal
Release
  • EAS Build
  • App Store submission
  • Bundle ai.usport.onpace
  • Shipped v1.0.4

Stack

React NativeExpo 54TypeScriptFirebase AuthFirestoreCloud FunctionsClaude SonnetNeo4j AuraVector RAGRevenueCatStripeEAS Build