Configuration
Environment Variables
Pierre Fitness Platform configured entirely via environment variables. No config files.
Required Variables
# database
DATABASE_URL="sqlite:./data/users.db" # or postgresql://...
# encryption (generate: openssl rand -base64 32)
PIERRE_MASTER_ENCRYPTION_KEY="<base64_encoded_32_bytes>"
Server Configuration
# network
HTTP_PORT=8081 # server port (default: 8081)
HOST=127.0.0.1 # bind address (default: 127.0.0.1)
# logging
RUST_LOG=info # log level (error, warn, info, debug, trace)
LOG_FORMAT=json # json or pretty (default: pretty)
LOG_INCLUDE_LOCATION=1 # include file/line numbers (production: auto-enabled)
LOG_INCLUDE_THREAD=1 # include thread information (production: auto-enabled)
LOG_INCLUDE_SPANS=1 # include tracing spans (production: auto-enabled)
Logging and Observability
Pierre provides production-ready logging with structured output, request correlation, and performance monitoring.
HTTP Request Logging
Automatic HTTP request/response logging via tower-http TraceLayer:
what gets logged:
- request: method, URI, HTTP version
- response: status code, latency (milliseconds)
- request ID: unique UUID for correlation
example output (INFO level):
INFO request{method=GET uri=/health}: tower_http::trace::on_response status=200 latency=5ms
INFO request{method=POST uri=/auth/login}: tower_http::trace::on_response status=200 latency=45ms
INFO request{method=GET uri=/api/activities}: tower_http::trace::on_response status=200 latency=235ms
verbosity control:
RUST_LOG=tower_http=warn- disable HTTP request logsRUST_LOG=tower_http=info- enable HTTP request logs (default)RUST_LOG=tower_http=debug- add request/response headers
Structured Logging (JSON Format)
JSON format recommended for production deployments:
LOG_FORMAT=json
RUST_LOG=info
benefits:
- machine-parseable for log aggregation (Elasticsearch, Splunk, etc.)
- automatic field extraction for querying
- preserves structured data (no string parsing needed)
- efficient storage and indexing
fields included:
timestamp: ISO 8601 timestamp with millisecondslevel: log level (ERROR, WARN, INFO, DEBUG, TRACE)target: rust module path (e.g.,pierre_mcp_server::routes::auth)message: human-readable messagespan: tracing span context (operation, duration, fields)fields: structured key-value pairs
example json output:
{"timestamp":"2025-01-13T10:23:45.123Z","level":"INFO","target":"pierre_mcp_server::routes::auth","fields":{"route":"login","email":"user@example.com"},"message":"User login attempt for email: user@example.com"}
{"timestamp":"2025-01-13T10:23:45.168Z","level":"INFO","target":"tower_http::trace::on_response","fields":{"method":"POST","uri":"/auth/login","status":200,"latency_ms":45},"message":"request completed"}
pretty format (development default):
2025-01-13T10:23:45.123Z INFO pierre_mcp_server::routes::auth route=login email=user@example.com: User login attempt for email: user@example.com
2025-01-13T10:23:45.168Z INFO tower_http::trace::on_response method=POST uri=/auth/login status=200 latency_ms=45: request completed
Request ID Correlation
Every HTTP request receives unique X-Request-ID header for distributed tracing:
response header:
HTTP/1.1 200 OK
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
tracing through logs:
Find all logs for specific request:
# json format
cat logs/pierre.log | jq 'select(.fields.request_id == "550e8400-e29b-41d4-a716-446655440000")'
# pretty format
grep "550e8400-e29b-41d4-a716-446655440000" logs/pierre.log
benefits:
- correlate logs across microservices
- debug user-reported issues via request ID
- trace request flow through database, APIs, external providers
- essential for production troubleshooting
Performance Monitoring
Automatic timing spans for critical operations:
database operations:
#![allow(unused)]
fn main() {
#[tracing::instrument(skip(self), fields(db_operation = "get_user"))]
async fn get_user(&self, user_id: Uuid) -> Result<Option<User>>
}
provider api calls:
#![allow(unused)]
fn main() {
#[tracing::instrument(skip(self), fields(provider = "strava", api_call = "get_activities"))]
async fn get_activities(&self, limit: Option<usize>) -> Result<Vec<Activity>>
}
route handlers:
#![allow(unused)]
fn main() {
#[tracing::instrument(skip(self, request), fields(route = "login", email = %request.email))]
pub async fn login(&self, request: LoginRequest) -> AppResult<LoginResponse>
}
example performance logs:
DEBUG pierre_mcp_server::database db_operation=get_user user_id=123e4567-e89b-12d3-a456-426614174000 duration_ms=12
INFO pierre_mcp_server::providers::strava provider=strava api_call=get_activities duration_ms=423
INFO pierre_mcp_server::routes::auth route=login email=user@example.com duration_ms=67
analyzing performance:
# find slow database queries (>100ms)
cat logs/pierre.log | jq 'select(.fields.db_operation and .fields.duration_ms > 100)'
# find slow API calls (>500ms)
cat logs/pierre.log | jq 'select(.fields.api_call and .fields.duration_ms > 500)'
# average response time per route
cat logs/pierre.log | jq -r 'select(.fields.route) | "\(.fields.route) \(.fields.duration_ms)"' | awk '{sum[$1]+=$2; count[$1]++} END {for (route in sum) print route, sum[route]/count[route]}'
Security and Privacy
no sensitive data logged:
- JWT secrets never logged (removed in production-ready improvements)
- passwords never logged (hashed before storage)
- OAuth tokens never logged (encrypted at rest)
- PII redacted by default (emails masked in non-auth logs)
verified security:
# verify no JWT secrets in logs
RUST_LOG=debug cargo run 2>&1 | grep -i "secret\|password\|token" | grep -v "access_token"
# should show: no JWT secret exposure, only generic "initialized successfully" messages
safe to log:
- user IDs (UUIDs, not emails)
- request IDs (correlation)
- operation types (login, get_activities, etc.)
- performance metrics (duration, status codes)
- error categories (not full stack traces with sensitive data)
MCP Tool Configuration
Control which MCP tools are available to tenants via environment variables and admin API.
Global Tool Disabling
# Comma-separated list of tool names to globally disable
PIERRE_DISABLED_TOOLS=analyze_sleep_quality,suggest_rest_day,track_sleep_trends,optimize_sleep_schedule
Use cases:
- Disable tools requiring premium provider integrations (e.g., sleep tools need WHOOP/Garmin)
- Temporarily disable tools during maintenance or outages
- Restrict tools based on deployment environment (dev vs production)
precedence (highest to lowest):
- Global disabled (
PIERRE_DISABLED_TOOLS) - overrides everything - Plan restrictions - subscription tier limits
- Tenant overrides - per-tenant admin configuration
- Tool catalog defaults - tool’s built-in enabled state
Per-Tenant Tool Overrides
Admin API endpoints for managing tool availability per tenant:
# List tool catalog
GET /admin/tools/catalog
# Get effective tools for tenant
GET /admin/tools/tenant/{tenant_id}
# Enable/disable tool for tenant
POST /admin/tools/tenant/{tenant_id}/override
{
"tool_name": "analyze_sleep_quality",
"is_enabled": false,
"reason": "Provider not configured"
}
# Remove override (revert to default)
DELETE /admin/tools/tenant/{tenant_id}/override/{tool_name}
# Get availability summary
GET /admin/tools/tenant/{tenant_id}/summary
# List globally disabled tools
GET /admin/tools/global-disabled
required permissions:
view_configuration: read-only access to catalog and tenant toolsmanage_configuration: create/delete tool overrides
Authentication
# jwt tokens
JWT_EXPIRY_HOURS=24 # token lifetime (default: 24)
JWT_SECRET_PATH=/path/to/secret # optional: load secret from file
PIERRE_RSA_KEY_SIZE=4096 # rsa key size for rs256 signing (default: 4096, test: 2048)
# oauth2 server
OAUTH2_ISSUER_URL=http://localhost:8081 # oauth2 discovery issuer url (default: http://localhost:8081)
# password hashing
PASSWORD_HASH_ALGORITHM=argon2 # argon2 or bcrypt (default: argon2)
Fitness Providers
strava
STRAVA_CLIENT_ID=your_id
STRAVA_CLIENT_SECRET=your_secret
STRAVA_REDIRECT_URI=http://localhost:8081/api/oauth/callback/strava # local development only
security warning: http callback urls only for local development. Production must use https:
STRAVA_REDIRECT_URI=https://api.example.com/api/oauth/callback/strava # production
Get credentials: https://www.strava.com/settings/api
Garmin
GARMIN_CLIENT_ID=your_consumer_key
GARMIN_CLIENT_SECRET=your_consumer_secret
GARMIN_REDIRECT_URI=http://localhost:8081/api/oauth/callback/garmin # local development only
security warning: http callback urls only for local development. Production must use https:
GARMIN_REDIRECT_URI=https://api.example.com/api/oauth/callback/garmin # production
Get credentials: https://developer.garmin.com/
Whoop
WHOOP_CLIENT_ID=your_client_id
WHOOP_CLIENT_SECRET=your_client_secret
WHOOP_REDIRECT_URI=http://localhost:8081/api/oauth/callback/whoop # local development only
security warning: http callback urls only for local development. Production must use https:
WHOOP_REDIRECT_URI=https://api.example.com/api/oauth/callback/whoop # production
Get credentials: https://developer.whoop.com/
whoop capabilities:
- Sleep tracking (sleep sessions, sleep stages, sleep need)
- Recovery metrics (HRV, recovery score, strain)
- Workout activities (with heart rate zones, strain scores)
- Health metrics (SpO2, skin temperature, body measurements)
whoop scopes:
offline: Required for refresh tokensread:profile: User profile informationread:body_measurement: Height, weight, max heart rateread:workout: Workout/activity dataread:sleep: Sleep tracking dataread:recovery: Recovery scoresread:cycles: Physiological cycle data
Terra
Terra provides unified access to 150+ wearable devices through a single API.
TERRA_API_KEY=your_api_key
TERRA_DEV_ID=your_dev_id
TERRA_WEBHOOK_SECRET=your_webhook_secret # for webhook data ingestion
Get credentials: https://tryterra.co/
terra capabilities:
- Unified API for 150+ wearables (Garmin, Polar, WHOOP, Oura, etc.)
- Webhook-based data ingestion
- Activity, sleep, and health data aggregation
Fitbit
FITBIT_CLIENT_ID=your_id
FITBIT_CLIENT_SECRET=your_secret
FITBIT_REDIRECT_URI=http://localhost:8081/api/oauth/callback/fitbit # local development only
security warning: http callback urls only for local development. Production must use https:
FITBIT_REDIRECT_URI=https://api.example.com/api/oauth/callback/fitbit # production
Get credentials: https://dev.fitbit.com/apps
callback url security:
- http: local development only (
localhostor127.0.0.1)- tokens transmitted unencrypted
- vulnerable to mitm attacks
- some providers reject http in production
- https: production deployments (required)
- tls encryption protects tokens in transit
- prevents credential interception
- required by most oauth providers in production
OpenWeather (Optional)
For weather-based recommendations:
OPENWEATHER_API_KEY=your_api_key
Get key: https://openweathermap.org/api
Algorithm Configuration
Fitness intelligence algorithms configurable via environment variables. Each algorithm has multiple variants with different accuracy, performance, and data requirements.
Max Heart Rate Estimation
PIERRE_MAXHR_ALGORITHM=tanaka # default
available algorithms:
fox: Classic 220 - age formula (simple, least accurate)tanaka: 208 - (0.7 × age) (default, validated in large studies)nes: 211 - (0.64 × age) (most accurate for fit individuals)gulati: 206 - (0.88 × age) (gender-specific for females)
Training Impulse (TRIMP)
PIERRE_TRIMP_ALGORITHM=hybrid # default
available algorithms:
bannister_male: Exponential formula for males (exp(1.92), requires resting HR)bannister_female: Exponential formula for females (exp(1.67), requires resting HR)edwards_simplified: Zone-based TRIMP (5 zones, linear weighting)lucia_banded: Sport-specific intensity bands (cycling, running)hybrid: Auto-select Bannister if data available, fallback to Edwards (default)
Training Stress Score (TSS)
PIERRE_TSS_ALGORITHM=avg_power # default
available algorithms:
avg_power: Fast calculation using average power (default, always works)normalized_power: Industry standard using 30s rolling window (requires power stream)hybrid: Try normalized power, fallback to average power if stream unavailable
VDOT (Running Performance)
PIERRE_VDOT_ALGORITHM=daniels # default
available algorithms:
daniels: Jack Daniels’ formula (VO2 = -4.60 + 0.182258×v + 0.000104×v²) (default)riegel: Power-law model (T2 = T1 × (D2/D1)^1.06) (good for ultra distances)hybrid: Auto-select Daniels for 5K-Marathon, Riegel for ultra distances
Training Load (CTL/ATL/TSB)
PIERRE_TRAINING_LOAD_ALGORITHM=ema # default
available algorithms:
ema: Exponential Moving Average (TrainingPeaks standard, CTL=42d, ATL=7d) (default)sma: Simple Moving Average (equal weights, simpler but less responsive)wma: Weighted Moving Average (linear weights, compromise between EMA and SMA)kalman: Kalman Filter (optimal for noisy data, complex tuning)
Recovery Aggregation
PIERRE_RECOVERY_ALGORITHM=weighted # default
available algorithms:
weighted: Weighted average with physiological priorities (default)additive: Simple sum of recovery scoresmultiplicative: Product of normalized recovery factorsminmax: Minimum score (conservative, limited by worst metric)neural: ML-based aggregation (requires training data)
Functional Threshold Power (FTP)
PIERRE_FTP_ALGORITHM=from_vo2max # default
available algorithms:
20min_test: 95% of 20-minute max average power (most common field test)8min_test: 90% of 8-minute max average power (shorter alternative)ramp_test: Protocol-specific extraction (Zwift, TrainerRoad formats)60min_power: 100% of 60-minute max average power (gold standard, very difficult)critical_power: 2-parameter model (requires multiple test durations)from_vo2max: Estimate from VO2max (FTP = VO2max × 13.5 × fitness_factor) (default)hybrid: Try best available method based on recent activity data
Lactate Threshold Heart Rate (LTHR)
PIERRE_LTHR_ALGORITHM=from_maxhr # default
available algorithms:
from_maxhr: 85-90% of max HR based on fitness level (default, simple)from_30min: 95-100% of 30-minute test average HR (field test)from_race: Extract from race efforts (10K-Half Marathon pace)lab_test: Direct lactate measurement (requires lab equipment)hybrid: Auto-select best method from available data
VO2max Estimation
PIERRE_VO2MAX_ALGORITHM=from_vdot # default
available algorithms:
from_vdot: Calculate from running VDOT (VO2max = VDOT in ml/kg/min) (default)cooper: 12-minute run test (VO2max = (distance_m - 504.9) / 44.73)rockport: 1-mile walk test (considers HR, age, gender, weight)astrand: Submaximal cycle test (requires HR response)bruce: Treadmill protocol (clinical setting, progressive grades)hybrid: Auto-select from available test data
algorithm selection strategy:
- default algorithms: balanced accuracy vs data requirements
- hybrid algorithms: defensive programming, fallback to simpler methods
- specialized algorithms: higher accuracy but more data/computation required
configuration example (.envrc):
# conservative setup (less data required)
export PIERRE_MAXHR_ALGORITHM=tanaka
export PIERRE_TRIMP_ALGORITHM=edwards_simplified
export PIERRE_TSS_ALGORITHM=avg_power
export PIERRE_RECOVERY_ALGORITHM=weighted
# performance setup (requires more data)
export PIERRE_TRIMP_ALGORITHM=bannister_male
export PIERRE_TSS_ALGORITHM=normalized_power
export PIERRE_TRAINING_LOAD_ALGORITHM=kalman
export PIERRE_RECOVERY_ALGORITHM=neural
Database Configuration
sqlite (development)
DATABASE_URL="sqlite:./data/users.db"
Creates database file at path if not exists.
PostgreSQL (Production)
DATABASE_URL="postgresql://user:pass@localhost:5432/pierre"
# connection pool
POSTGRES_MAX_CONNECTIONS=20 # max pool size (default: 20)
POSTGRES_MIN_CONNECTIONS=2 # min pool size (default: 2)
POSTGRES_ACQUIRE_TIMEOUT=30 # connection timeout seconds (default: 30)
SQLx Pool Configuration
Fine-tune database connection pool behavior for production workloads:
# connection lifecycle
SQLX_IDLE_TIMEOUT_SECS=600 # close idle connections after (default: 600)
SQLX_MAX_LIFETIME_SECS=1800 # max connection lifetime (default: 1800)
# connection validation
SQLX_TEST_BEFORE_ACQUIRE=true # validate before use (default: true)
# performance
SQLX_STATEMENT_CACHE_CAPACITY=100 # prepared statement cache (default: 100)
Tokio Runtime Configuration
Configure async runtime for performance tuning:
# worker threads (default: number of CPU cores)
TOKIO_WORKER_THREADS=4
# thread stack size in bytes (default: OS default)
TOKIO_THREAD_STACK_SIZE=2097152 # 2MB
# worker thread name prefix (default: pierre-worker)
TOKIO_THREAD_NAME=pierre-worker
Cache Configuration
# cache configuration (in-memory or redis)
CACHE_MAX_ENTRIES=10000 # max cached items for in-memory (default: 10,000)
CACHE_CLEANUP_INTERVAL_SECS=300 # cleanup interval in seconds (default: 300)
# redis cache (optional - uses in-memory if not set)
REDIS_URL=redis://localhost:6379 # redis connection url
Rate Limiting
# burst limits per tier (requests in short window)
RATE_LIMIT_FREE_TIER_BURST=100 # default: 100
RATE_LIMIT_PROFESSIONAL_BURST=500 # default: 500
RATE_LIMIT_ENTERPRISE_BURST=2000 # default: 2000
# OAuth2 endpoint rate limits (requests per minute)
OAUTH_AUTHORIZE_RATE_LIMIT_RPM=60 # default: 60
OAUTH_TOKEN_RATE_LIMIT_RPM=30 # default: 30
OAUTH_REGISTER_RATE_LIMIT_RPM=10 # default: 10
# Admin-provisioned API key monthly limit (Starter tier default)
PIERRE_ADMIN_API_KEY_MONTHLY_LIMIT=10000
Security
# cors
CORS_ALLOWED_ORIGINS="http://localhost:3000,http://localhost:5173"
CORS_MAX_AGE=3600
# csrf protection
CSRF_TOKEN_EXPIRY=3600 # seconds
# tls (production)
TLS_CERT_PATH=/path/to/cert.pem
TLS_KEY_PATH=/path/to/key.pem
Fitness Configuration
User-specific fitness parameters managed via mcp tools or rest api.
Configuration Profiles
Predefined fitness profiles:
beginner: conservative zones, longer recoveryintermediate: standard zones, moderate trainingadvanced: aggressive zones, high training loadelite: performance-optimized zonescustom: user-defined parameters
Fitness Parameters
{
"profile": "advanced",
"vo2_max": 55.0,
"max_heart_rate": 185,
"resting_heart_rate": 45,
"threshold_heart_rate": 170,
"threshold_power": 280,
"threshold_pace": 240,
"weight_kg": 70.0,
"height_cm": 175
}
Training Zones
Automatically calculated based on profile:
{
"heart_rate_zones": [
{"zone": 1, "min_bpm": 93, "max_bpm": 111},
{"zone": 2, "min_bpm": 111, "max_bpm": 130},
{"zone": 3, "min_bpm": 130, "max_bpm": 148},
{"zone": 4, "min_bpm": 148, "max_bpm": 167},
{"zone": 5, "min_bpm": 167, "max_bpm": 185}
],
"power_zones": [
{"zone": 1, "min_watts": 0, "max_watts": 154},
{"zone": 2, "min_watts": 154, "max_watts": 210},
...
]
}
Updating Configuration
Via mcp tool:
{
"tool": "update_user_configuration",
"parameters": {
"profile": "elite",
"vo2_max": 60.0,
"threshold_power": 300
}
}
Via rest api:
curl -X PUT http://localhost:8081/api/configuration/user \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"profile": "elite",
"vo2_max": 60.0
}'
Configuration Catalog
Get all available parameters:
curl -H "Authorization: Bearer <jwt>" \
http://localhost:8081/api/configuration/catalog
Response describes each parameter:
- type (number, boolean, enum)
- valid range
- default value
- description
Using direnv
Recommended for local development.
Setup
brew install direnv
# add to shell (~/.zshrc or ~/.bashrc)
eval "$(direnv hook zsh)" # or bash
# in project directory
direnv allow
.envrc File
Edit .envrc in project root:
# development overrides
export RUST_LOG=debug
export HTTP_PORT=8081
export DATABASE_URL=sqlite:./data/users.db
# provider credentials (dev)
export STRAVA_CLIENT_ID=dev_client_id
export STRAVA_CLIENT_SECRET=dev_secret
export STRAVA_REDIRECT_URI=http://localhost:8081/api/oauth/callback/strava
# load from file
if [ -f .env.local ]; then
source .env.local
fi
Direnv automatically loads/unloads environment when entering/leaving directory.
.env.local (Gitignored)
Store secrets in .env.local:
# never commit this file
export PIERRE_MASTER_ENCRYPTION_KEY="<generated_key>"
export STRAVA_CLIENT_SECRET="<real_secret>"
Production Deployment
environment file
Create /etc/pierre/environment:
DATABASE_URL=postgresql://pierre:pass@db.internal:5432/pierre
PIERRE_MASTER_ENCRYPTION_KEY=<strong_key>
HTTP_PORT=8081
HOST=0.0.0.0
LOG_FORMAT=json
RUST_LOG=info
# provider credentials from secrets manager
STRAVA_CLIENT_ID=prod_id
STRAVA_CLIENT_SECRET=prod_secret
STRAVA_REDIRECT_URI=https://api.example.com/api/oauth/callback/strava
# tls
TLS_CERT_PATH=/etc/pierre/tls/cert.pem
TLS_KEY_PATH=/etc/pierre/tls/key.pem
# postgres
POSTGRES_MAX_CONNECTIONS=50
POSTGRES_MIN_CONNECTIONS=5
# cache
CACHE_MAX_ENTRIES=50000
# rate limiting
RATE_LIMIT_REQUESTS_PER_MINUTE=120
systemd Service
[Unit]
Description=Pierre MCP Server
After=network.target postgresql.service
[Service]
Type=simple
User=pierre
Group=pierre
WorkingDirectory=/opt/pierre
EnvironmentFile=/etc/pierre/environment
ExecStart=/opt/pierre/bin/pierre-mcp-server
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Docker
FROM rust:1.70 as builder
WORKDIR /build
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /build/target/release/pierre-mcp-server /usr/local/bin/
ENV HTTP_PORT=8081
ENV DATABASE_URL=postgresql://pierre:pass@db:5432/pierre
EXPOSE 8081
CMD ["pierre-mcp-server"]
Run:
docker run -d \
--name pierre \
-p 8081:8081 \
-e DATABASE_URL=postgresql://... \
-e PIERRE_MASTER_ENCRYPTION_KEY=... \
pierre:latest
Validation
Check configuration at startup:
RUST_LOG=info cargo run --bin pierre-mcp-server
Logs show:
- loaded environment variables
- database connection status
- enabled features
- configured providers
- listening address
Troubleshooting
missing environment variables
Server fails to start. Check required variables set:
echo $DATABASE_URL
echo $PIERRE_MASTER_ENCRYPTION_KEY
Invalid Database URL
- sqlite: ensure directory exists
- postgresql: verify connection string, credentials, database exists
Provider OAuth Fails
- verify redirect uri exactly matches environment variable
- ensure uri accessible from browser (not
127.0.0.1for remote) - check provider console for correct credentials
Port Conflicts
Change http_port:
export HTTP_PORT=8082
Encryption Key Errors
Regenerate:
openssl rand -base64 32
Must be exactly 32 bytes (base64 encoded = 44 characters).
References
All configuration constants: src/constants/mod.rs
Fitness profiles: src/config/profiles.rs
Database setup: src/database_plugins/