AI-native training planning, workout logging, analytics, and coaching for Hyrox-style hybrid athletes.
Features | Tech Stack | Architecture | Project Structure | Getting Started | Scripts | Testing | CI/CD | License
fitai.coach helps athletes plan structured training, log complex workouts, understand progress, and adjust upcoming sessions with coach-grade AI context. It combines a timeline-first training log, plan import and generation, structured exercise tables, training-style controls, Strava and Garmin sync, RAG-backed coaching materials, and privacy controls that keep AI features opt-in.
- Interactive timeline - View past, current, and upcoming training with planned, completed, missed, and skipped states.
- Structured workout logging - Log free-text, voice-entered, table-backed, or block-based sessions with sets, reps, loads, distances, times, custom exercise names, notes, and scores, plus optional session-level distance and average/max heart rate for athletes logging manually without a synced wearable.
- Training plans - Import CSV plans, start from built-in programming, or generate a plan with AI.
- Training styles - Choose balanced programming or MAF Method constraints, with MAF setup fields used to calculate and persist the athlete's heart-rate ceiling. MAF athletes can tag any logged workout, including synced Strava runs, as a MAF test to track pace at the same heart rate over time.
- Timeline annotations - Mark injury, illness, travel, rest, or other date ranges so analytics and training gaps have context.
- Guided onboarding - Configure profile, units, goals, schedule, and initial plan setup before landing in the main app.
- Modular text AI provider layer - Text features default to Gemini and can be routed by operators to Anthropic or OpenAI-compatible providers such as OpenAI, xAI, Groq, Together, OpenRouter, DeepSeek, or a custom base URL.
- Workout parsing - Turn text or voice like
"3 sets bench 225lbs x 8, then 3 miles in 24 min"into validated structured exercise data. - Photo-to-workout parsing - Upload an image of a whiteboard, plan sheet, or coach notes and extract workout structure with Gemini vision.
- Streaming coach chat - Ask context-aware questions over Server-Sent Events with recent training, plan status, and coaching materials in scope.
- Auto-coach and suggestions - Evaluate completed workouts, fatigue signals, plan phase, station gaps, and RAG materials to propose targeted updates.
- Coaching material uploads - Add CSV, DOCX, or PDF materials to enrich the coach with athlete-specific or coach-specific guidance.
- Vector retrieval - Documents are chunked, embedded with Gemini embeddings, and searched through pgvector for retrieval-augmented coaching.
- Fallback-aware retrieval - The app can fall back to legacy full-text materials when embeddings are missing, mismatched, or unavailable.
- Strava - OAuth sync imports recent activities, deduplicates them per user, and stores encrypted tokens.
- Garmin Connect - Email/password Garmin SSO sync imports recent activities with encrypted credentials and a strict safety stack: per-user locks, rate limits, minimum sync interval, global 429 circuit breaker, and audit logging.
- Training overview - Track volume, duration, average workouts per week, completion rate, streaks, and week-over-week changes.
- Exercise progression - See personal records, set history, category breakdowns, and progression trends.
- Coach insights - Surface RPE trends, plan phase, weekly volume, station gaps, fatigue flags, and progression flags.
- MAF Trend - For MAF Method athletes, chart per-test compliance and pace-at-ceiling progression with classification badges across tagged MAF tests.
- Data export - Download workout timeline and exercise sets as CSV or JSON.
- Email and push notifications - Send opt-in weekly summaries and missed-day reminders through pg-boss, Resend, and Web Push when configured.
- AI consent gate - AI coach features are opt-in through
aiCoachEnabled; new users default to disabled. - Runtime AI kill switch - Operators can disable AI provider traffic with
AI_FEATURES_ENABLED=false. - Account deletion -
DELETE /api/v1/accountremoves Clerk identity data where possible and cascade-deletes user-owned app records. - Privacy page - A first-party privacy page lists third-party processors and the data each receives.
- Installable app - Vite PWA and Workbox provide installability and offline-aware behavior.
- Browser push - Web Push subscriptions can deliver training reminders to opted-in devices when VAPID credentials are configured.
- Offline feedback - The client surfaces offline/drop notifications so failed interactions are visible.
This is a full-stack TypeScript monorepo with a React SPA, an Express API, shared schemas, Drizzle-backed PostgreSQL storage, pgvector-backed retrieval, and a modular AI provider layer.
- Framework: React 18, Vite 8, TypeScript 6
- Styling: Tailwind CSS 4 with shadcn/ui-style Radix primitives
- State management: TanStack Query for server state and cache invalidation
- Routing: wouter
- Drag and drop: dnd-kit
- Charts: Recharts
- PWA: vite-plugin-pwa and Workbox
- Error tracking: Sentry React, optional by environment
- Runtime: Node.js >=20, Express 4, TypeScript 6
- Database: PostgreSQL with Drizzle ORM
- Vector search: pgvector, optionally on a separate
VECTOR_DATABASE_URL - Authentication: Clerk JWT middleware with local dev bypass support
- AI text providers: Gemini by default, Anthropic, and OpenAI-compatible adapters
- Gemini-specific services: embeddings and image parsing through
@google/genai - Jobs and scheduling: pg-boss plus node-cron
- Email: Resend
- Push notifications: Web Push with VAPID keys
- Logging: Pino and pino-http
- API docs: Swagger UI generated from Zod/OpenAPI schemas
- Validation: Zod and drizzle-zod
- Helmet security headers and production CSP support
- CORS allowlist with credentials
- CSRF protection through a double-submit cookie flow
- Server-side idempotency for mutating API requests with
X-Idempotency-Key - AES-256-GCM encryption for Strava and Garmin credentials/tokens
- Rate limiting on sensitive and high-cost endpoints
- HTML sanitization for AI-generated content
- Startup env validation for production-only invariants such as
CSRF_SECRET, weak key rejection, and auth bypass lockout
- Shared Drizzle tables, Zod schemas, enums, types, and OpenAPI registry in
shared/ - Client and server import shared types directly through TypeScript path aliases
flowchart TB
subgraph Client["Client (React SPA)"]
UI["Vite + React 18"]
TQ["TanStack Query"]
SW["Service Worker / PWA"]
end
subgraph Server["Express API"]
API["Route Handlers"]
Services["Service Layer"]
TextAI["Text AI Provider Layer"]
Queue["pg-boss Queue"]
end
subgraph Data["Data Layer"]
PG[("PostgreSQL")]
PGV[("PostgreSQL + pgvector")]
end
subgraph External["External Services"]
Clerk["Clerk Auth"]
Strava["Strava OAuth"]
Garmin["Garmin Connect SSO"]
Resend["Resend Email"]
Gemini["Gemini Embeddings + Vision"]
Anthropic["Anthropic"]
OpenAICompatible["OpenAI-compatible Providers"]
end
UI --> TQ --> API
SW -. "offline cache" .-> UI
API --> Services
Services --> PG
Services --> PGV
Services --> TextAI
TextAI --> Gemini
TextAI --> Anthropic
TextAI --> OpenAICompatible
Services --> Gemini
API --> Clerk
Services --> Strava
Services --> Garmin
Queue --> Resend
Queue --> PG
flowchart LR
subgraph Inputs["Athlete Inputs"]
Voice["Voice Input"]
Text["Free Text"]
Photo["Workout Photo"]
Docs["Coaching Materials"]
end
subgraph AI["AI Services"]
Provider["Text Provider Facade"]
Vision["Gemini Vision"]
Embeddings["Gemini Embeddings"]
Retrieval["pgvector Retrieval"]
end
subgraph Storage["Storage"]
DB[("Workout + Plan Data")]
VDB[("Document Chunks")]
end
Voice --> Provider
Text --> Provider
Photo --> Vision
Docs --> Embeddings --> VDB --> Retrieval
Provider --> DB
Vision --> DB
Retrieval --> Provider
Hyrox-Companion/
|-- client/ # React frontend (Vite SPA)
| |-- public/ # Brand assets, favicon, PWA assets
| `-- src/
| |-- components/ # UI, timeline, coach, analytics, settings, workout surfaces
| |-- hooks/ # Custom React hooks
| |-- lib/ # API client, query client, utilities
| `-- pages/ # Landing, Timeline, LogWorkout, Analytics, Settings, Privacy
|-- server/ # Express backend
| |-- ai/providers/ # Gemini, Anthropic, and OpenAI-compatible text adapters
| |-- bootstrap/ # Startup, health, observability, and shutdown wiring
| |-- gemini/ # Gemini client, parsing, image, chat, suggestion helpers
| |-- middleware/ # CSP nonce, CSRF, idempotency, AI budget/consent
| |-- prompts/ # AI prompt builders and coaching-context formatters
| |-- routes/ # Route modules and workout route groups
| |-- services/ # Business logic, AI context, RAG, analytics, plans
| |-- storage/ # Drizzle-backed data access
| |-- usecases/ # Use-case orchestration layer
| `-- utils/ # Server utilities
|-- shared/ # Drizzle schema, Zod types, OpenAPI registry
|-- migrations/ # Drizzle SQL migrations
|-- cypress/ # End-to-end specs and support
|-- docs/ # Living subsystem docs and OpenAPI snapshot
|-- script/ # Build, maintenance, benchmark, and docs scripts
|-- .github/workflows/ # CI workflows
`-- README.md
Detailed documentation lives in docs/:
| Document | Description |
|---|---|
| Architecture Overview | End-to-end flows, provider layer, service dependencies, RAG decision tree, schema pipeline |
| Environment Variables | Required and optional env vars, defaults, feature gates, safety invariants |
| Client | React SPA, routing, components, styling, PWA, client Sentry |
| Server | Express bootstrap, middleware stack, routes, logging, graceful shutdown |
| Database | PostgreSQL schema, Drizzle ORM, pgvector, migrations, storage layer |
| AI and RAG | Text provider layer, Gemini embeddings/vision, coaching context, RAG pipeline |
| State Management | TanStack Query, custom hooks, offline queue, utility functions |
| API Reference | Endpoint catalog, request/response shapes, rate limits |
| Authentication | Clerk setup, user sync, dev auth bypass, protected routes |
| Integrations | Strava, Garmin, Resend, pg-boss, cron, Sentry |
| Testing | Vitest, integration tests, Cypress, accessibility checks, CI workflows |
| Native Mobile | Capacitor vs. React Native comparison and packaging phases |
Interactive Swagger UI is available at /api/docs when the server is running in development. The OpenAPI document is generated from the shared Zod registry through @asteasolutions/zod-to-openapi.
A committed OpenAPI 3.0 snapshot is kept at docs/openapi.json. Regenerate it with:
pnpm docs:openapiThe Build workflow fails if the committed snapshot drifts from the generated spec.
- Node.js 20 or newer
- pnpm 9.12.x through Corepack (
corepack enable) - PostgreSQL with the pgvector extension
- Optional: Clerk keys for real authentication
- Optional: Gemini, Anthropic, or OpenAI-compatible provider keys for AI text features
- Optional: Strava, Resend, Sentry, and Web Push credentials for their integrations
Copy the example file:
cp .env.example .envMinimum local boot variables:
| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
ENCRYPTION_KEY |
32+ character secret for AES-256-GCM encryption |
Generate a strong local key with:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Production also requires CSRF_SECRET, and it must differ from ENCRYPTION_KEY. See .env.example and Environment Variables for the full reference.
For local development without Clerk, set:
ALLOW_DEV_AUTH_BYPASS=truepnpm install
pnpm run db:migrateUse pnpm run db:generate only when you intentionally change shared/schema/tables.ts and need to create a new migration.
pnpm devThe app serves the React frontend and Express API on port 5000. Visit http://localhost:5000.
| Script | Description |
|---|---|
pnpm dev |
Start the development server with .env loaded |
pnpm build |
Build the client and server for production |
pnpm start |
Run the production build from dist/ |
pnpm check |
Run TypeScript type checking |
pnpm test |
Run the Vitest unit test suite |
pnpm test:watch |
Run Vitest in watch mode |
pnpm test:smoke |
Run the fast smoke suite via vitest.smoke.config.ts |
pnpm lint |
Run ESLint |
pnpm lint:fix |
Auto-fix ESLint issues |
pnpm format |
Format the repo with Prettier |
pnpm format:check |
Check formatting without writing files |
pnpm db:generate |
Generate a new Drizzle migration after schema changes |
pnpm db:migrate |
Run pending Drizzle migrations |
pnpm db:check |
Validate migration/schema consistency |
pnpm db:decode-entities |
Decode stored HTML entities in workout text |
pnpm coach:influence |
Run the AI coach influence harness |
pnpm docs:openapi |
Regenerate docs/openapi.json |
pnpm bench:timeline |
Run the timeline benchmark |
pnpm bench:timeline:check |
Run the timeline benchmark guard |
postinstall runs script/patch-cypress-deps.js to patch Cypress transitive dependencies.
| Layer | Tool | Command |
|---|---|---|
| Unit tests | Vitest | pnpm test |
| Integration tests | Vitest with PostgreSQL | pnpm exec vitest run --config vitest.integration.config.ts |
| Production smoke tests | Vitest smoke config | pnpm test:smoke |
| End-to-end tests | Cypress | pnpm exec cypress open or pnpm exec cypress run |
| Accessibility checks | jest-axe via Vitest | pnpm test |
| Type safety | TypeScript 6 | pnpm check |
| Linting | ESLint | pnpm lint |
| Formatting | Prettier | pnpm format:check |
The current suite includes 237 Vitest test files plus 12 Cypress E2E specs. See Testing for exact setup, local database requirements, Cypress conventions, and CI details.
GitHub Actions workflows live in .github/workflows/:
| Workflow | Trigger | Purpose |
|---|---|---|
| Build | Push to main, pull request |
ESLint, TypeScript, OpenAPI snapshot drift check |
| Unit Tests | Push to main, pull request |
Vitest unit suite |
| Cypress Tests | Push | Build, integration tests, smoke tests, Cypress with PostgreSQL/pgvector |
| Check Migrations | Push to main, pull request |
Drizzle migration consistency |
| Post-Migration Verification | Manual | Apply migrations and verify a real Neon database |
| Dependency Review | Pull request | Audit dependency changes |
| DevSkim | Push to main, pull request, weekly schedule |
Static security scanning |
| Bearer | Push to main, pull request, weekly schedule |
Security and privacy scanning |
SonarQube Cloud automatic analysis is configured outside the manual workflow steps.
The app targets WCAG 2.1 AA. It uses Radix primitives for focus management, supports keyboard navigation, includes a skip-to-content link, respects reduced-motion preferences, and has automated jest-axe checks for key interactive components.
When reporting an accessibility issue, include the page, browser, assistive technology, expected behavior, and actual behavior.
- Fork the project.
- Create a feature branch.
- Make the smallest focused change that solves the problem.
- Run the relevant checks, usually
pnpm check,pnpm test, andpnpm lint. - Update docs and
docs/openapi.jsonwhen the public API or setup changes. - Open a pull request with the behavior change, verification, and any remaining risks.
This project is licensed under the MIT License.