An AI endurance coach for iPhone and Apple Watch — swim, bike, run, and triathlon. CoachSage reads your Apple Health data (HRV, resting heart rate, sleep, workouts) and asks Claude — via a small backend proxy — for personalized training advice: a daily recommendation, a weekly training plan, goal and race-day plans, and a chat interface that remembers what you've told it.
Started as a personal, single-user project named RunCoach; it now ships TestFlight builds under the name CoachSage, heading to App Store 1.0. The repo and bundle IDs keep the original name.
Sources/RunCoach/ SwiftUI iOS app (HealthKit, chat, plans, settings)
Sources/RunCoachWidget/ iOS Home/Lock Screen widget extension
Sources/RunCoachWatch/ watchOS app — a thin snapshot shell (no HealthKit/network)
Sources/RunCoachWatchWidget/ watchOS complication extension
Sources/Shared/ the ONLY code shared by app + extensions:
WidgetSnapshot contract, EffortStore, widget AppIntents
backend/ Node/Express proxy that calls the Anthropic API
app.js route wiring + shared middleware
lib/ prompts, auth, rate-limit, Anthropic client, validators
routes/ Express routers: /coach, /weekly-plan, /goal-plan,
/race-plan, /season-recap, /account, /analytics, /health
server.js local-dev entry point (app.listen)
api/index.js Vercel serverless entry (export default app)
eval/ LLM prompt-eval harness (21+ cases; costs real tokens)
sql/ one-shot DDL for the Supabase tables
test/ framework-less unit tests (npm test)
site/ marketing site + hosted privacy policy (coachsage.app)
project.yml XcodeGen spec — generates RunCoach.xcodeproj
docs/ design/ops docs (e.g. legacy-key deprecation plan)
scripts/ admin scripts (Supabase sign-in checks)
launchd/, logs/ leftovers from the retired self-hosted CI runner
.github/workflows/ CI on GitHub-hosted runners
- App → Backend → Claude. The app never holds an Anthropic API key — it
POSTs to a small Express service (deployed as a single Vercel serverless
function at
https://api.coachsage.app) which calls Claude and returns plain text / structured JSON. - Auth is Sign in with Apple → Supabase. The app exchanges the Apple
identity token for a Supabase session; every backend call carries that
session JWT, which the backend verifies against Supabase's JWKS endpoint
(
jose). The original shared-secretBACKEND_API_KEYwas retired in July 2026 — see docs/legacy-key-deprecation-plan.md. - Two models:
claude-haiku-4-5for/coachchat (cheap, frequent),claude-sonnet-4-6for the structured plan endpoints (/weekly-plan,/goal-plan,/race-plan), with JSON shape validation + one retry. - HealthKit (read-only) supplies recovery metrics (HRV vs your own
baseline, resting HR, sleep) and recent workouts. The app assembles a
healthContextstring on-device (ContextBuilder) and attaches it to outgoing requests — health data is never persisted server-side. - Plans. Weekly plans regenerate every Sunday evening and "heal"
visibly mid-week when sessions are missed or recovery is red; goal/macro
plans periodize base→build→peak→taper toward an event; race-day plans
cover pacing, fueling, and checklists. R3 adds a multi-event A/B/C season
model so training races and tune-ups are embedded inside the A-event build.
Athlete thresholds (threshold pace, FTP, CSS, threshold HR) are estimated
from best recent efforts, confirmed in Settings, and flagged stale over
time. An injury/availability status pauses sports with
[PAUSED]semantics and a graduated return to training. - Coach memory. The model can tag durable facts in chat replies
(
⟦REMEMBER: …⟧); once confirmed, they're injected into every later prompt (capped, synced viauser_state). - Share cards, season recap & race-day Live Activity. Finisher-moment and race-plan cards render to images for sharing; a completed A-race triggers an AI season recap; and on race day the race plan can start a Live Activity on the Lock Screen / Dynamic Island.
- Swim depth. Swim workouts show laps, total strokes and an average SWOLF score computed from per-length HealthKit data.
- One snapshot everywhere. The iOS widget, the Apple Watch app, and the
watch complication all render the same
WidgetSnapshot— written to a shared App Group container and pushed to the watch over WatchConnectivity. RPE check-ins can be logged from the app, the interactive widget, or Siri (App Intents). - Persistence. No Core Data — Codable structs in UserDefaults with a
central key registry (
PersistenceKeys), and aStateSyncactor that mirrors coaching state to a Supabaseuser_staterow (debounced, last-write-wins) so reinstalls and second devices restore. - Backend endpoints:
POST /coach,POST /weekly-plan,POST /goal-plan,POST /race-plan,POST /season-recap,DELETE /account,POST /analytics,GET /health(public). Per-user rate limiting (12/min, 60/day) is backed by a Supabase table; analytics are self-hosted and privacy-first (no user IDs). - Background work. A ~6am
BGAppRefreshTaskrefreshes the day's recommendation, widget, and morning notification; HealthKitHKObserverQuerybackground delivery drives the post-workout debrief.
xcodegen generate
open RunCoach.xcodeprojBuild and run on a physical device — the simulator has no HealthKit data. On first launch, sign in with Apple and grant HealthKit permissions; the app talks to the production backend by default.
The backend (backend/) is deployed on Vercel as a single serverless
function behind https://api.coachsage.app. It auto-deploys on every push
to main (Vercel's GitHub integration, Root Directory = backend/).
ANTHROPIC_API_KEY, SUPABASE_URL, and SUPABASE_SERVICE_ROLE_KEY are
set as encrypted environment variables in the Vercel project — never
committed. Without the Supabase vars, auth is disabled and every protected
route returns 401.
cd backend
npm install
cp .env.example .env # fill in ANTHROPIC_API_KEY (+ Supabase vars to exercise auth)
npm startVerify with curl localhost:3000/health → {"ok":true}. Authenticated
routes need a real Supabase user JWT (Authorization: Bearer <jwt>) — the
eval harness sources one via EVAL_JWT.
The backend is structured to deploy to Vercel unchanged — no separate build step:
backend/app.js— route wiring and shared middlewarebackend/lib/— prompts, Anthropic client, auth, rate-limiting, validatorsbackend/routes/— Express routers for each endpointbackend/server.js— thin local-dev entry point (app.listen(...))backend/api/index.js— Vercel serverless entry (export default app; Express apps are natively(req, res) => {}-compatible)backend/vercel.json— catch-all rewrite (/(.*) → /api) plusmaxDuration: 60for the longer plan calls (the default 10s would be too tight)
To redeploy from scratch: import the GitHub repo into Vercel, set Root
Directory to backend/, and add the three environment variables above.
-
iOS unit tests (
Tests/, XCTest) — pure-logic suites for the math, policy, and prompt-builder types, plus orchestration tests against faked dependency seams (Dependencies.swift). They build unsigned with no host app, so they run fast on any simulator:xcodegen generate xcodebuild test -project RunCoach.xcodeproj -scheme RunCoach \ -destination 'platform=iOS Simulator,name=iPhone 17'
-
UI tests (
UITests/, XCUITest) — a small suite driving the real app in a seeded demo mode (COACHSAGE_DEMO=1). -
Backend —
cd backend && npm test: four framework-less suites (auth gate, rate limiter, body validation, plan-response validators). -
Prompt evals —
BACKEND_URL=… EVAL_JWT=… node backend/eval/run-evals.mjs: 21 structural assertions against a running backend. Costs real tokens and is deliberately not in CI; run it before deploying prompt changes.
HealthKit behavior needs a physical device — see TESTING.md for the manual checklist and AppStore.md / PRIVACY_POLICY.md for the App Store listing drafts.
CI (.github/workflows/ci.yml) runs on every push
to any branch and on PRs to main, on GitHub-hosted runners:
Build iOS App(macos-latest) — installs xcodegen, generates the project, thenxcodebuild teston the first available iPhone simulator (device names vary between hosted images, so the workflow resolves one at runtime).Check Backend(ubuntu-latest, Node 20) —npm ci,node --check, the four test suites, andnpm audit --omit=dev --audit-level=high.
There's no deploy job: the backend auto-deploys via Vercel's GitHub integration, and app distribution is a manual Xcode Archive → App Store Connect (TestFlight) step.
- The
.xcodeprojis gitignored and regenerated byxcodegen generatein CI (and locally) fromproject.yml— don't hand-edit the generated project file, changes won't persist. xcodebuildoutput is consumed raw in CI rather than piped through a formatter — a pipe likexcodebuild | xcbeautify | truewould silently swallow build failures, since the pipeline's exit status would reflect the formatter, notxcodebuild.