Skip to content

PrernaWeb/RunCoach

Repository files navigation

RunCoach (CoachSage)

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.

Project layout

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

Architecture

  • 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-secret BACKEND_API_KEY was retired in July 2026 — see docs/legacy-key-deprecation-plan.md.
  • Two models: claude-haiku-4-5 for /coach chat (cheap, frequent), claude-sonnet-4-6 for 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 healthContext string 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 via user_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 a StateSync actor that mirrors coaching state to a Supabase user_state row (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 BGAppRefreshTask refreshes the day's recommendation, widget, and morning notification; HealthKit HKObserverQuery background delivery drives the post-workout debrief.

Getting started

Run the app

xcodegen generate
open RunCoach.xcodeproj

Build 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.

Backend — deployed on Vercel

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.

Local development

cd backend
npm install
cp .env.example .env   # fill in ANTHROPIC_API_KEY (+ Supabase vars to exercise auth)
npm start

Verify 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.

How the Vercel deploy works

The backend is structured to deploy to Vercel unchanged — no separate build step:

  • backend/app.js — route wiring and shared middleware
  • backend/lib/ — prompts, Anthropic client, auth, rate-limiting, validators
  • backend/routes/ — Express routers for each endpoint
  • backend/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) plus maxDuration: 60 for 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.

Testing

  • 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).

  • Backendcd backend && npm test: four framework-less suites (auth gate, rate limiter, body validation, plan-response validators).

  • Prompt evalsBACKEND_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

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, then xcodebuild test on 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, and npm 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.

Gotchas worth knowing about

  • The .xcodeproj is gitignored and regenerated by xcodegen generate in CI (and locally) from project.yml — don't hand-edit the generated project file, changes won't persist.
  • xcodebuild output is consumed raw in CI rather than piped through a formatter — a pipe like xcodebuild | xcbeautify | true would silently swallow build failures, since the pipeline's exit status would reflect the formatter, not xcodebuild.

About

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors