What it is
A production-grade training-planning platform I built solo — Rails 8 REST API, React 19 + TypeScript (strict) SPA, PostgreSQL 17 — designed so every analytics report is reachable by Claude Code (or any MCP client) as a structured agent tool. The app doubles as an AI-native workbench: you can ask an agent "show me my ACWR projection" and it calls the same whitelisted report the UI uses.
Why it exists
I wanted a training platform that treats AI agents as first-class operators, not a tacked-on chatbot. Most fitness apps hand agents a bag of generic "logSet / getHistory" tools; that's fine for simple automations but useless for coaching decisions like "is my ACWR ratio drifting", "am I under-stimulating chest", or "did this deload actually work". Those questions need proper reports — the same structured, validated, whitelist-safe endpoints the UI already uses. So the design constraint was: every analytics answer is a named tool, backed by Ruby code, never by an LLM making up SQL.
Second driver: a domain I actually train in. Periodization, volume tracking, ACWR, and body-composition comparisons need a rich domain model (plans → blocks → macrocycles → microcycles → routines → sets), not a flat log. Building the whole stack end-to-end gave me a real testbed for Rails 8 + React 19 + MCP integration patterns I wanted to know deeply.
Architecture
The Rails backend is a monolith with clear service-object and registry boundaries. The 45 analytics reports live under app/services/analytics/reports/* grouped by concern (strength, cardio, hypertrophy, load, periodization, compliance, body-composition, plan). A central Analytics::Registry maps stable report_key strings to report classes — that whitelist is the only way Api::ReportsController#show can instantiate a report, which closes the "LLM writes SQL" door entirely. Each report declares its arg schema; the companion MCP server consumes the registry over HTTP and exposes one tool per report with typed parameters.
What I built
- Analytics Registry + 45 reports —
app/services/analytics/registry.rbis a strict whitelist mappingreport_key→Analytics::Reports::*class. Groups: strength (tonnage-by-week, intensity distribution, e1RM trend, PR timeline, working-sets-by-lift, relative strength), cardio (zone distribution, weekly-distance-time, TRIMP), hypertrophy (weekly sets by muscle, volume by equipment, stimulus heatmap), load (ACWR monitor, monotony/strain, fitness-fatigue-form), periodization (block summary, block-over-block, deload effectiveness), compliance (completion rate, scope drift, planned-vs-actual), body composition (weight vs target, calorie balance, circumference changes), and comparison reports (intensity/tonnage/volume compare, ACWR projection compare). - MCP tool surface — each registered report is exposed via a companion MCP server as a typed tool. Dev/dev2 run local MCP servers; a test environment has a global MCP endpoint wired into Claude Code for daily agent-driven workflows. Prod path is scaffolded (
docker-compose.prod.yml+.env.prod) but not yet deployed. - Multi-tenant domain — plan → block → macrocycle → microcycle → routine → workout-session → set hierarchy. Multi-tenancy applies at plan / organization / workspace / team scopes via Pundit policies (13 policy classes). 60 models with rich associations (periodization models: linear, DUP, WUP, block, conjugate, concurrent).
- Security-first auth — Devise + TOTP 2FA, email activation before login,
DeviceTokencomposite-key system (user_id + device_id + user_agent_hash) with automatic revocation on password-change / 2FA-disable. Environment-aware email headers (TEST/DEV in subject line) prevent test-traffic confusion. - Import pipelines — CSV importers for daily metrics, measurements, recovery data, and routine definitions. Each importer splits parsing (
*_csv_parser_service) from persistence (*_import_service) so validation is testable in isolation. - Multi-env Docker Compose —
docker-compose.{dev,test,prod}.ymlwith env-specific.env.*files (git-crypt encrypted), per-env deploy scripts, and centralized PostgreSQL + Redis shared across environments.deploy.shdetects environment from hostname and applies zero-touch rebuilds. - Path-scoped Claude rules —
.claude/rules/*(rails-backend, react-frontend, database, documentation, testing, dry-principles, project-invariants) with glob-scoped frontmatter. Claude Code auto-loads the relevant rule set when editing files that match the glob. - 32 MCP server tools for operations on the platform side (via Claude Code's own MCP config): Plane (project management), Perplexity (web search), Grafana (dashboards + PromQL), PostgreSQL (read-only), n8n (workflow automation). Plus 32 custom Claude Code skill commands for repeated ops work.
Results
- ~66k LOC total — ~18k Ruby backend + ~48k TypeScript frontend.
- 60 ActiveRecord models · 41 API controllers · 64 services · 13 Pundit policies · 86 migrations.
- 45 whitelisted analytics reports in
Analytics::Registry, each individually addressable as an MCP tool. - Zero LLM-generated SQL — every report is a Ruby class with explicit data access; registry lookup refuses unknown
report_keys. - Multi-env orchestration — dev, dev2, and test environments live and reproducible from
.env.*+docker-compose.*.yml. Prod path scaffolded (not yet deployed).
Stack
Rails 8, Ruby 3.x, PostgreSQL 17, Redis, Devise + TOTP, Pundit, Solid Queue, Solid Cable, Kamal (deploy), React 19, TypeScript (strict), TanStack Query, React Hook Form, Radix, Tailwind, Vitest, Storybook, Docker, Docker Compose, git-crypt, Model Context Protocol (MCP).
Status
- Repo: private.
- Running: dev / dev2 local, test on a dedicated LXC.
- MCP: Claude Code connects to the test environment's MCP endpoint for daily agent-driven analytics and plan operations; local dev MCP servers cover isolated workflows.
- Related portfolio entries:
claude-code-mcp-operator(how MCP is wired across my fleet).


