Architecture
Pierre Fitness Platform is a multi-protocol fitness data platform that connects AI assistants to strava, garmin, fitbit, whoop, coros, and terra (150+ wearables). Single binary, single port (8081), multiple protocols.
System Design
┌─────────────────┐
│ mcp clients │ claude desktop, chatgpt, etc
└────────┬────────┘
│
▼
┌─────────────────┐
│ pierre sdk │ typescript bridge (stdio → http)
│ (npm package) │
└────────┬────────┘
│ http + oauth2
▼
┌─────────────────────────────────────────┐
│ Pierre Fitness Platform (rust) │
│ port 8081 (all protocols) │
│ │
│ • mcp protocol (json-rpc 2.0) │
│ • oauth2 server (rfc 7591) │
│ • a2a protocol (agent-to-agent) │
│ • rest api │
│ • sse (real-time notifications) │
└────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ fitness providers (1 to x) │
│ • strava │
│ • garmin │
│ • fitbit │
│ • whoop │
│ • coros │
│ • synthetic (oauth-free dev/testing) │
│ • custom providers (pluggable) │
│ │
│ ProviderRegistry: runtime discovery │
│ Environment config: PIERRE_*_* │
└─────────────────────────────────────────┘
Core Components
Protocols Layer (src/protocols/)
universal/- protocol-agnostic business logic- shared by mcp and a2a protocols
- dozens of fitness tools (activities, analysis, goals, sleep, recovery, nutrition, configuration)
MCP Implementation (src/mcp/)
- json-rpc 2.0 over http
- sse transport for streaming
- tool registry and execution
OAuth2 Server (src/oauth2_server/)
- rfc 7591 dynamic client registration
- rfc 7636 pkce support
- jwt access tokens for mcp clients
OAuth2 Client (src/oauth2_client/)
- pierre connects to fitness providers as oauth client
- pkce support for enhanced security
- automatic token refresh
- multi-tenant credential isolation
Providers (src/providers/)
- pluggable provider architecture: factory pattern with runtime registration
- feature flags: compile-time provider selection (
provider-strava,provider-garmin,provider-fitbit,provider-whoop,provider-coros,provider-terra,provider-synthetic) - service provider interface (spi):
ProviderDescriptortrait for external provider registration - bitflags capabilities: efficient
ProviderCapabilitieswith combinators (full_health(),full_fitness()) - 1 to x providers simultaneously: supports strava + garmin + custom providers at once
- provider registry:
ProviderRegistrymanages all providers with dynamic discovery - environment-based config: cloud-native configuration via
PIERRE_<PROVIDER>_*env vars:PIERRE_STRAVA_CLIENT_ID,PIERRE_STRAVA_CLIENT_SECRET(also: legacySTRAVA_CLIENT_ID)PIERRE_<PROVIDER>_AUTH_URL,PIERRE_<PROVIDER>_TOKEN_URL,PIERRE_<PROVIDER>_SCOPES- Falls back to hardcoded defaults if env vars not set
- shared
FitnessProvidertrait: uniform interface for all providers - built-in providers: strava, garmin, fitbit, whoop, coros, terra (150+ wearables), synthetic (oauth-free dev/testing)
- oauth parameters:
OAuthParamscaptures provider-specific oauth differences (scope separator, pkce) - dynamic discovery:
supported_providers()andis_supported()for runtime introspection - zero code changes: add new providers without modifying tools or connection handlers
- unified oauth token management: per-provider credentials with automatic refresh
Intelligence (src/intelligence/)
- activity analysis and insights
- performance trend detection
- training load calculation
- goal feasibility analysis
Database (src/database/)
- repository pattern: 14 focused repositories following SOLID principles
- repository accessors:
db.users(),db.oauth_tokens(),db.api_keys(),db.profiles(), etc. - pluggable backend (sqlite, postgresql) via
src/database_plugins/ - encrypted token storage
- multi-tenant isolation
Repository Architecture
The database layer implements the repository pattern with focused, cohesive repositories:
14 focused repositories (src/database/repositories/):
UserRepository- user account managementOAuthTokenRepository- oauth token storage (tenant-scoped)ApiKeyRepository- api key managementUsageRepository- usage tracking and analyticsA2ARepository- agent-to-agent managementProfileRepository- user profiles and goalsInsightRepository- ai-generated insightsAdminRepository- admin token managementTenantRepository- multi-tenant managementOAuth2ServerRepository- oauth 2.0 server functionalitySecurityRepository- key rotation and auditNotificationRepository- oauth notificationsFitnessConfigRepository- fitness configuration managementRecipeRepository- recipe and nutrition management
accessor pattern (src/database/mod.rs:139-245):
#![allow(unused)]
fn main() {
let db = Database::new(database_url, encryption_key).await?;
// Access repositories via typed accessors
let user = db.users().get_by_id(user_id).await?;
let token = db.oauth_tokens().get(user_id, tenant_id, provider).await?;
let api_key = db.api_keys().get_by_key(key).await?;
}
benefits:
- single responsibility: each repository handles one domain
- interface segregation: consumers only depend on needed methods
- testability: mock individual repositories independently
- maintainability: changes isolated to specific repositories
Authentication (src/auth.rs)
- jwt token generation/validation
- api key management
- rate limiting per tenant
Error Handling
Pierre Fitness Platform uses structured error types for precise error handling and propagation. The codebase does not use anyhow - all errors are structured types using thiserror.
Error Type Hierarchy
AppError (src/errors.rs)
├── Database(DatabaseError)
├── Provider(ProviderError)
├── Authentication
├── Authorization
├── Validation
└── Internal
Error Types
DatabaseError (src/database/errors.rs):
NotFound: entity not found (user, token, oauth client)QueryFailed: database query execution failureConstraintViolation: unique constraint or foreign key violationsConnectionFailed: database connection issuesTransactionFailed: transaction commit/rollback errors
ProviderError (src/providers/errors.rs):
ApiError: fitness provider api errors (status code + message)AuthenticationFailed: oauth token invalid or expiredRateLimitExceeded: provider rate limit hitNetworkError: network connectivity issuesUnavailable: provider temporarily unavailable
AppError (src/errors.rs):
- application-level errors with error codes
- http status code mapping
- structured error responses with context
Error Propagation
All fallible operations return Result<T, E> types with structured error types only:
#![allow(unused)]
fn main() {
pub async fn get_user(db: &Database, user_id: &str) -> Result<User, DatabaseError>
pub async fn fetch_activities(provider: &Strava) -> Result<Vec<Activity>, ProviderError>
pub async fn process_request(req: Request) -> Result<Response, AppError>
}
AppResult type alias (src/errors.rs):
#![allow(unused)]
fn main() {
pub type AppResult<T> = Result<T, AppError>;
}
Errors propagate using ? operator with automatic conversion via From trait implementations:
#![allow(unused)]
fn main() {
// DatabaseError converts to AppError via From<DatabaseError>
let user = db.users().get_by_id(user_id).await?;
// ProviderError converts to AppError via From<ProviderError>
let activities = provider.fetch_activities().await?;
}
no blanket anyhow conversions: the codebase enforces zero-tolerance for impl From<anyhow::Error> via static analysis (scripts/lint-and-test.sh) to prevent loss of type information.
Error Responses
Structured json error responses:
{
"error": {
"code": "database_not_found",
"message": "User not found: user-123",
"details": {
"entity_type": "user",
"entity_id": "user-123"
}
}
}
Http status mapping:
DatabaseError::NotFound→ 404ProviderError::ApiError→ 502/503AppError::Validation→ 400AppError::Authentication→ 401AppError::Authorization→ 403
Implementation: src/errors.rs, src/database/errors.rs, src/providers/errors.rs
Request Flow
client request
↓
[security middleware] → cors, headers, csrf
↓
[authentication] → jwt or api key
↓
[tenant context] → load user/tenant data
↓
[rate limiting] → check quotas
↓
[protocol router]
├─ mcp → universal protocol → tools
├─ a2a → universal protocol → tools
└─ rest → direct handlers
↓
[tool execution]
├─ providers (strava/garmin/fitbit/whoop/coros)
├─ intelligence (analysis)
└─ configuration
↓
[database + cache]
↓
response
Multi-Tenancy
Every request operates within tenant context:
- isolated data per tenant
- tenant-specific encryption keys
- custom rate limits
- feature flags
Key Design Decisions
Single Port Architecture
All protocols share port 8081. Simplified deployment, easier oauth2 callback handling, unified tls/security.
Focused Context Dependency Injection
Replaces service locator anti-pattern with focused contexts providing type-safe DI with minimal coupling.
context hierarchy (src/context/):
ServerContext
├── AuthContext (auth_manager, auth_middleware, admin_jwt_secret, jwks_manager)
├── DataContext (database, provider_registry, activity_intelligence)
├── ConfigContext (config, tenant_oauth_client, a2a_client_manager)
└── NotificationContext (websocket_manager, oauth_notification_sender)
usage pattern:
#![allow(unused)]
fn main() {
// Access specific contexts from ServerContext
let user = ctx.data().database().users().get_by_id(id).await?;
let token = ctx.auth().auth_manager().validate_token(jwt)?;
}
benefits:
- single responsibility: each context handles one domain
- interface segregation: handlers depend only on needed contexts
- testability: mock individual contexts independently
- type safety: compile-time verification of dependencies
migration: ServerContext::from(&ServerResources) provides gradual migration path.
Protocol Abstraction
Business logic in protocols::universal works for both mcp and a2a. Write once, use everywhere.
Pluggable Architecture
- database: sqlite (dev) or postgresql (prod)
- cache: in-memory lru or redis (distributed caching)
- tools: compile-time plugin system via
linkme
Runtime SQL Queries
The codebase uses sqlx::query() (runtime validation) exclusively, not sqlx::query!() (compile-time validation).
Why runtime queries:
- Multi-database support: SQLite and PostgreSQL have different SQL dialects (
?1vs$1). Compile-time macros lock to one database. - No build-time database:
query!macros requireDATABASE_URLat compile time. Runtime queries allow building without a database. - CI simplicity: No need for
sqlx prepareor database containers during builds. - Plugin architecture:
DatabaseProvidertrait enables runtime database selection.
Trade-off:
- No compile-time SQL validation - typos caught at runtime, not build time
- Mitigated by comprehensive integration tests against both databases
Implementation: src/database_plugins/mod.rs (trait), src/database_plugins/postgres.rs, src/database/
SDK Architecture
TypeScript SDK (sdk/): stdio→http bridge for MCP clients (Claude Desktop, ChatGPT).
MCP Client (Claude Desktop)
↓ stdio (json-rpc)
pierre-mcp-client (npm package)
↓ http (json-rpc)
Pierre MCP Server (rust)
key features:
- automatic oauth2 token management (browser-based auth flow)
- token refresh handling
- secure credential storage via system keychain
- npx deployment:
npx -y pierre-mcp-client@next --server http://localhost:8081
Implementation: sdk/src/bridge.ts, sdk/src/cli.ts
Type Mapping System
rust→typescript type generation: auto-generates TypeScript interfaces from server JSON schemas.
src/mcp/schema.rs (tool definitions)
↓ npm run generate-types
sdk/src/types.ts (47 parameter interfaces)
type-safe json schemas (src/types/json_schemas.rs):
- replaces dynamic
serde_json::Valuewith typed structs - compile-time validation via serde
- fail-fast error handling with clear error messages
- backwards compatibility via field aliases (
#[serde(alias = "type")])
generated types include:
ToolParamsMap- maps tool names to parameter typesToolName- union type of all 47 tool names- common data types:
Activity,Athlete,Stats,FitnessConfig
Usage: npm run generate-types (requires running server on port 8081)
File Structure
src/
├── bin/
│ ├── pierre-mcp-server.rs # main binary
│ ├── admin_setup.rs # admin cli tool (binary: admin-setup)
│ └── diagnose_weather_api.rs # weather api diagnostic tool
├── protocols/
│ └── universal/ # shared business logic
├── mcp/ # mcp protocol
├── oauth2_server/ # oauth2 authorization server (mcp clients → pierre)
├── oauth2_client/ # oauth2 client (pierre → fitness providers)
├── a2a/ # a2a protocol
├── providers/ # fitness integrations
├── intelligence/ # activity analysis
├── database/ # repository pattern (14 focused repositories)
│ ├── repositories/ # repository trait definitions and implementations
│ └── ... # user, oauth token, api key management modules
├── database_plugins/ # database backends (sqlite, postgresql)
├── admin/ # admin authentication
├── context/ # focused di contexts (auth, data, config, notification)
├── auth.rs # authentication
├── tenant/ # multi-tenancy
├── tools/ # tool execution engine
├── cache/ # caching layer
├── config/ # configuration
├── constants/ # constants and defaults
├── crypto/ # encryption utilities
├── types/ # type-safe json schemas
└── lib.rs # public api
sdk/ # typescript mcp client
├── src/bridge.ts # stdio→http bridge
├── src/types.ts # auto-generated types
└── test/ # integration tests
Security Layers
- transport: https/tls
- authentication: jwt tokens, api keys
- authorization: tenant-based rbac
- encryption: two-tier key management
- master key: encrypts tenant keys
- tenant keys: encrypt user tokens
- rate limiting: token bucket per tenant
- atomic operations: toctou prevention
- refresh token consumption: atomic check-and-revoke
- prevents race conditions in token exchange
- database-level atomicity guarantees
Scalability
Horizontal Scaling
Stateless server design. Scale by adding instances behind load balancer. Shared postgresql and optional redis for distributed cache.
Database Sharding
- tenant-based sharding
- time-based partitioning for historical data
- provider-specific tables
Caching Strategy
- health checks: 30s ttl
- mcp sessions: lru cache (10k entries)
- weather data: configurable ttl
- distributed cache: redis support for multi-instance deployments
- in-memory fallback: lru cache with automatic eviction
Plugin Lifecycle
Compile-time plugin system using linkme crate for intelligence modules.
Plugins stored in src/intelligence/plugins/:
- zone-based intensity analysis
- training recommendations
- performance trend detection
- goal feasibility analysis
Lifecycle hooks:
init()- plugin initializationexecute()- tool executionvalidate()- parameter validationcleanup()- resource cleanup
Plugins registered at compile time via #[distributed_slice(PLUGINS)] attribute.
No runtime loading, zero overhead plugin discovery.
Implementation: src/intelligence/plugins/mod.rs, src/lifecycle/
Algorithm Dependency Injection
Zero-overhead algorithm dispatch using rust enums instead of hardcoded formulas.
Design Pattern
Fitness intelligence uses enum-based dependency injection for all calculation algorithms:
#![allow(unused)]
fn main() {
pub enum VdotAlgorithm {
Daniels, // Jack Daniels' formula
Riegel { exponent: f64 }, // Power-law model
Hybrid, // Auto-select based on data
}
impl VdotAlgorithm {
pub fn calculate_vdot(&self, distance: f64, time: f64) -> Result<f64, AppError> {
match self {
Self::Daniels => Self::calculate_daniels(distance, time),
Self::Riegel { exponent } => Self::calculate_riegel(distance, time, *exponent),
Self::Hybrid => Self::calculate_hybrid(distance, time),
}
}
}
}
Benefits
compile-time dispatch: zero runtime overhead, inlined by llvm configuration flexibility: runtime algorithm selection via environment variables defensive programming: hybrid variants with automatic fallback testability: each variant independently testable maintainability: all algorithm logic in single enum file no magic strings: type-safe algorithm selection
Algorithm Types
Nine algorithm categories with multiple variants each:
-
max heart rate (
src/intelligence/algorithms/max_heart_rate.rs)- fox, tanaka, nes, gulati
- environment:
PIERRE_MAXHR_ALGORITHM
-
training impulse (trimp) (
src/intelligence/algorithms/trimp.rs)- bannister male/female, edwards, lucia, hybrid
- environment:
PIERRE_TRIMP_ALGORITHM
-
training stress score (tss) (
src/intelligence/algorithms/tss.rs)- avg_power, normalized_power, hybrid
- environment:
PIERRE_TSS_ALGORITHM
-
vdot (
src/intelligence/algorithms/vdot.rs)- daniels, riegel, hybrid
- environment:
PIERRE_VDOT_ALGORITHM
-
training load (
src/intelligence/algorithms/training_load.rs)- ema, sma, wma, kalman filter
- environment:
PIERRE_TRAINING_LOAD_ALGORITHM
-
recovery aggregation (
src/intelligence/algorithms/recovery_aggregation.rs)- weighted, additive, multiplicative, minmax, neural
- environment:
PIERRE_RECOVERY_ALGORITHM
-
functional threshold power (ftp) (
src/intelligence/algorithms/ftp.rs)- 20min_test, 8min_test, ramp_test, from_vo2max, hybrid
- environment:
PIERRE_FTP_ALGORITHM
-
lactate threshold heart rate (lthr) (
src/intelligence/algorithms/lthr.rs)- from_maxhr, from_30min, from_race, lab_test, hybrid
- environment:
PIERRE_LTHR_ALGORITHM
-
vo2max estimation (
src/intelligence/algorithms/vo2max_estimation.rs)- from_vdot, cooper, rockport, astrand, bruce, hybrid
- environment:
PIERRE_VO2MAX_ALGORITHM
Configuration Integration
Algorithms configured via src/config/intelligence/algorithms.rs:
#![allow(unused)]
fn main() {
pub struct AlgorithmConfig {
pub max_heart_rate: String, // PIERRE_MAXHR_ALGORITHM
pub trimp: String, // PIERRE_TRIMP_ALGORITHM
pub tss: String, // PIERRE_TSS_ALGORITHM
pub vdot: String, // PIERRE_VDOT_ALGORITHM
pub training_load: String, // PIERRE_TRAINING_LOAD_ALGORITHM
pub recovery_aggregation: String, // PIERRE_RECOVERY_ALGORITHM
pub ftp: String, // PIERRE_FTP_ALGORITHM
pub lthr: String, // PIERRE_LTHR_ALGORITHM
pub vo2max: String, // PIERRE_VO2MAX_ALGORITHM
}
}
Defaults optimized for balanced accuracy vs data requirements.
Enforcement
Automated validation ensures no hardcoded algorithms bypass the enum system.
Validation script: scripts/validate-algorithm-di.sh
Patterns defined: scripts/validation-patterns.toml
Checks for:
- hardcoded formulas (e.g.,
220 - age) - magic numbers (e.g.,
0.182258in non-algorithm files) - algorithmic logic outside enum implementations
Exclusions documented in validation patterns (e.g., tests, algorithm enum files).
Ci pipeline fails on algorithm di violations (zero tolerance).
Hybrid Algorithms
Special variant that provides defensive fallback logic:
#![allow(unused)]
fn main() {
pub enum TssAlgorithm {
AvgPower, // Simple, always works
NormalizedPower { .. }, // Accurate, requires power stream
Hybrid, // Try NP, fallback to avg_power
}
impl TssAlgorithm {
fn calculate_hybrid(&self, activity: &Activity, ...) -> Result<f64, AppError> {
Self::calculate_np_tss(activity, ...)
.or_else(|_| Self::calculate_avg_power_tss(activity, ...))
}
}
}
Hybrid algorithms maximize reliability while preferring accuracy when data available.
Usage Pattern
All intelligence calculations use algorithm enums:
#![allow(unused)]
fn main() {
use crate::intelligence::algorithms::vdot::VdotAlgorithm;
use crate::config::intelligence_config::get_config;
let config = get_config();
let algorithm = VdotAlgorithm::from_str(&config.algorithms.vdot)?;
let vdot = algorithm.calculate_vdot(5000.0, 1200.0)?; // 5K in 20:00
}
No hardcoded formulas anywhere in intelligence layer.
Implementation: src/intelligence/algorithms/, src/config/intelligence/algorithms.rs, scripts/validate-algorithm-di.sh
PII Redaction
Middleware layer removes sensitive data from logs and responses.
Redacted fields:
- email addresses
- passwords
- tokens (jwt, oauth, api keys)
- user ids
- tenant ids
Redaction patterns:
- email:
***@***.*** - token:
[REDACTED-<type>] - uuid:
[REDACTED-UUID]
Enabled via LOG_FORMAT=json for structured logging.
Implementation: src/middleware/redaction.rs
Cursor Pagination
Keyset pagination using composite cursor (created_at, id) for consistent ordering.
Benefits:
- no duplicate results during data changes
- stable pagination across pages
- efficient for large datasets
Cursor format: base64-encoded json with timestamp (milliseconds) + id.
Example:
cursor: "eyJ0aW1lc3RhbXAiOjE3MDAwMDAwMDAsImlkIjoiYWJjMTIzIn0="
decoded: {"timestamp":1700000000,"id":"abc123"}
Endpoints using cursor pagination:
GET /admin/users/pending?cursor=<cursor>&limit=20GET /admin/users/active?cursor=<cursor>&limit=20
Implementation: src/pagination/, src/database/users.rs:668-737, src/database_plugins/postgres.rs:378-420
Monitoring
Health endpoint: GET /health
- database connectivity
- provider availability
- system uptime
- cache statistics
Logs: structured json via tracing + opentelemetry Metrics: request latency, error rates, provider api usage