Freely hosted Records, Evaluation & Processing Server
A self-hosted server that collects health and training data from Apple Health, Oura, Withings, Hevy and Alpha Progression, stores it persistently, visualizes it through a web dashboard with freely configurable correlations, and exposes it as an MCP server for LLMs.
FreeReps for iOS 2.1 is coming to the App Store soon. The version in the App Store today, 1.0, stopped syncing on iOS 27. Version 2.1 syncs on iOS 27, sends only what HealthKit added since the last sync, and starts a sync from the app, a Home Screen widget or Siri. It needs a FreeReps server of the same release, 2.1. See iOS app.
- Daily overview — the four hero numbers are chosen per user, each with its own sparkline, above a table of every visible metric
- Correlation explorer — any metric against any other, as a scatter plot with an overlay, Pearson r, and r recomputed at four lags from one payload
- Sleep — hypnogram, stage composition, and HR, HRV and SpO2 through the night
- Workouts — heart rate zones against a maximum the user sets or FreeReps estimates from a date of birth, the GPS route on a map, and the sets of a strength session
- Metrics — time series with a moving average and a normal range band
- Trends — small multiples across the metric set, over a selectable window
- Settings — per-metric visibility, which metrics the ingest accepts, source priority per category, the integrations, the ingest log, and the alert channel
Each pair below is served in the colour scheme your client asks for. The MCP capture is a screenshot of Claude Desktop, so it has one version only.
| Dashboard | Sleep |
|---|---|
![]() |
![]() |
| Workouts | Metrics |
|---|---|
![]() |
![]() |
| Correlations | Trends |
|---|---|
![]() |
![]() |
| MCP |
|---|
![]() |
| iOS app | |||
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
FreeReps for iOS sends Apple Health data straight to your server:
- Only what changed — after the first backfill, each sync sends what HealthKit added since the previous one.
- A sync where you want it — the Sync tab, a Home Screen widget that shows the last sync, or the Shortcuts action "Sync Health Data" for Siri, the Action button and personal automations. There is no background sync: HealthKit is unreadable while the iPhone is locked.
- A fixed set of data — 38 HealthKit types covering activity, body, vitals, sleep, workouts with routes, blood pressure and state of mind. Diagnoses, clinical records, prescriptions and symptoms are not read.
- Your server decides — metrics switched off in Settings → Ingest are skipped before the app reads HealthKit.
Version 2.1 needs iOS 27 and a FreeReps server 2.1. Setup, the Shortcuts
triggers that work and the developer notes are in app/README.md.
Apple Health collects extensive data but offers no way to relate metrics to each other, no API for external analysis, and no export into a queryable system you own.
Other apps compute scores but are closed-source, subscription-based, and opaque. FreeReps takes the opposite approach: raw data + flexible visualization + LLM for interpretation.
Apple Health ──┐
(iPhone/Watch) │
│ Health Auto Export
│ ├─ REST automation ──── HTTPS POST ───┐
│ ├─ .hae in iCloud ──┐ │
│ └─ TCP/JSON-RPC ────┤ freereps-upload │
│ └──── HTTPS POST ──┤
│ │
FreeReps iOS app ───────────────────── HTTPS POST ───────┤
Alpha Progression CSV ──────── upload / POST /ingest/alpha┤
▼
Oura API v2 ←── OAuth2, 30 min ───┐ ┌──────────────────────────────┐
Withings API ←── OAuth2, 30 min ───┼───→│ FreeReps Server │
Hevy event feed←── API key, 30 min ──┘ │ │
│ Ingest ─→ Storage │
│ PostgreSQL + │
│ TimescaleDB │
│ │ │
│ source-priority dedup at │
│ query time, per category │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ Web MCP Alert │
│dashboard stdio + watcher│
│ HTTP /mcp │
└──┼───────────┼───────────┼───┘
▼ ▼ ▼
browser Claude ntfy topic
Every inbound channel writes into one store, and overlapping measurements are
resolved when a query runs rather than at ingest — see
DECISIONS.md, 2026-03-25.
- Privacy first — Measurements stay on your server. FreeReps sends nothing outbound except the calls to the sources you connect and, if you enable it, the alert POST to a topic you name. No telemetry.
- Self-hosted — Runs on your own server/homelab.
- Data over scores — Raw data + visualization + LLM instead of proprietary algorithms.
- Flexible over opinionated — Correlation explorer instead of hard-wired dashboards.
- Single binary — Go binary with embedded web UI.
| Component | Technology |
|---|---|
| Backend | Go (single binary with embedded frontend), chi router, pgx |
| Frontend | React 19 + Vite + TypeScript + Tailwind CSS 4 |
| Charts | uPlot (time series, sparklines, hypnogram), Leaflet (workout routes) |
| Database | PostgreSQL + TimescaleDB (hypertable on health_metrics) |
| Auth & Networking | Tailscale (tsnet) — zero-config TLS + identity |
| MCP | mcp-go, stdio + Streamable HTTP |
| Migrations | golang-migrate, applied at startup |
| Config | YAML, with FREEREPS_* environment overrides |
| Deployment | Docker Compose |
The Go toolchain version is pinned in server/go.mod, the
frontend dependency versions in
server/web/package.json.
- Tailscale — FreeReps uses Tailscale for authentication and TLS natively (via tsnet). There are no passwords or API keys — access is controlled by your tailnet. Tailscale must be set up before running FreeReps.
- An iPhone with Apple Health data, sending it through the FreeReps iOS app or Health Auto Export.
- mcp-proxy (optional) — Needed only by an MCP client that speaks stdio alone; it bridges stdio to the HTTP endpoint. Install with
brew install mcp-proxyorpip install mcp-proxy. lzfse(optional, macOS) — Required byfreereps-uploadfor reading.haefiles.brew install lzfse.
git clone https://github.com/meltforce/FreeReps.git
cd FreeReps/server
cp config.example.yaml config.yaml
# Edit config.yaml — set database password, enable Tailscale
docker compose up -dTo use the pre-built image from Docker Hub instead of building locally, replace the app service's build: . with image: meltforce/freereps:latest in docker-compose.yml.
Apple Health data reaches FreeReps through the FreeReps iOS app or through
Health Auto Export. Both post to /api/v1/ingest and both write their rows
with an empty source; the server tells them apart by the client
(health_metrics.client), and where both delivered the same window the app's
rows count — see DECISIONS.md, 2026-09-25.
- Install FreeReps on an iPhone in your tailnet.
- In Settings, set the host to the server's Tailscale name and grant the Apple Health permissions. On iOS 27, choose All Recorded Data in the second permission step; Past 30 Days hides everything older.
- Under Advanced, choose how far back the first sync reaches, then tap Full Sync on the Sync tab.
Later syncs send only what HealthKit added. The app identifies itself with
X-FreeReps-Client: freereps-ios; its ingests appear as freereps_ios in
Settings → Ingest. app/README.md has the details.
Health Auto Export reads HealthKit on the iPhone and delivers it over three paths, which can be combined:
| Path | What it is | Used for |
|---|---|---|
| REST automation | The app posts JSON to a URL on a schedule | Ongoing delivery — this is the default |
| TCP server connection | The app answers JSON-RPC queries on the local network | Historical backfill, via freereps-upload -hae-host |
.hae file export |
The app writes compressed files to iCloud Drive | Historical backfill, via freereps-upload -path |
Setting up the REST automation:
- Open Settings → Ingest in FreeReps and copy the server URL shown there
(
https://<your-host>/api/v1/ingest). - In Health Auto Export, create an automation of type REST API, paste that URL, and set the format to JSON.
- Select the metrics and workouts to export. Aggregation and period are the
app's own settings;
sinceLastSynckeeps each delivery to what is new. - Save it. Settings → Ingest lists the last 25 deliveries with the row counts each one carried.
The iPhone reaches the server over the tailnet, so the automation needs no API
key of its own — Tailscale authenticates the request. The payload shape FreeReps
accepts is written down in
server/specs/hae-rest-api.md.
Backfilling history: a REST automation delivers from the moment it is set
up. For everything before that, use freereps-upload in TCP mode against the
app's server connection, or in file mode against an iCloud export — see
Upload Tool.
FreeReps integrates directly with the Oura API v2 to pull ring data. Syncs every 30 minutes with 90-day initial backfill.
Data synced:
- Oura-exclusive — readiness score, sleep score, activity score, temperature deviation, stress, recovery, resilience, cardiovascular age
- Overlapping with Apple Watch — heart rate, HRV, SpO2, respiratory rate, steps, active calories, workouts, sleep sessions/stages
Source priority dedup: When both Oura and Apple Watch report the same metric, FreeReps deduplicates at query time using configurable source priority (Settings > Source Priority). Only the highest-priority source's data is shown — no double-counting.
-
Register an Oura API app at cloud.ouraring.com/oauth/applications:
- Redirect URI: the exact value shown under Settings > Oura Ring. FreeReps derives it from the address you reach it on, so it needs no configuration — see Redirect URIs if you need to pin it.
- Privacy Policy URL: your FreeReps website's privacy page
- Terms of Service URL: your FreeReps website's terms page
- Enable all scopes
-
Enter credentials in FreeReps: Go to Settings > Oura Ring, enter your Client ID and Client Secret, click "Save Credentials"
-
Authorize: Click "Authorize with Oura", approve access on Oura's page. You'll be redirected back to FreeReps.
-
Sync starts automatically every 30 minutes. Use "Sync Now" for immediate sync. Check Settings > Import Logs for sync status.
FreeReps reads weight, body composition and blood pressure directly from the Withings Public API. Syncs every 30 minutes with 90-day initial backfill.
Data synced: weight, fat ratio, fat mass, fat free mass, muscle mass, bone mass, body water, blood pressure (systolic/diastolic) and the pulse the cuff records with each reading.
The same measurements also reach FreeReps through Apple Health, where they arrive only once the Health app has synced. The default source priority puts Withings first, so the direct read wins wherever both cover a day. The Apple Health path is not disabled — it remains the only route for an installation without a Withings account.
-
Register an app in the Withings Partner Hub. The Public API tier requires no contract and no approval.
- Redirect URI: the exact value shown under Settings > Withings. It follows the address you reach FreeReps on, so renaming a host changes it — and the OAuth callback is the only place that breaks, because token refresh sends no redirect URI. See Redirect URIs.
- Scope:
user.metrics
-
Enter credentials in FreeReps: Settings > Withings, enter Client ID and Client Secret, click "Save credentials".
-
Authorize: Click "Authorize with Withings" and approve access. The authorization code is valid for 30 seconds, so complete the redirect rather than leaving the consent page open.
-
Sync starts automatically every 30 minutes. Use "Sync now" for an immediate run; Settings > Import Logs carries the outcome.
Both OAuth integrations need a redirect URI registered with the provider, and it has to match what FreeReps sends — the provider compares the value from the start of the flow with the one sent when the code is exchanged.
FreeReps derives it per request from the origin you reached it on:
https://<host>/oura/callback and https://<host>/withings/callback. A reverse
proxy's X-Forwarded-Proto and X-Forwarded-Host are honoured. The current value
is shown in the Settings tab of each integration, which is the value to paste into
the provider's form.
Pin it where the derived value is not stable or not the registered one — the UI answering on several names, or a proxy under a name the headers do not carry:
server:
base_url: "https://freereps.example.ts.net" # scheme and host onlyFREEREPS_SERVER_BASE_URL overrides the same value. A path in it is refused at
startup rather than producing a URI the provider rejects at the end of a flow.
Hevy is the strength training source. FreeReps polls
GET /v1/workouts/events?since= every 30 minutes, which carries creations,
updates and deletions, so a correction made in the app reaches the server on
the next run. Hevy's webhook is not used —
DECISIONS.md, 2026-08-04, has the reasoning.
Data synced: sessions with exercise, set, rep, weight and effort data. RPE and RIR are stored on their own scales rather than converted at ingest. The exercise catalog is pulled as well, which is what supplies the muscle group per exercise.
Derived from it: volume per muscle group, tonnage (SUM(weight_kg * reps),
external load only) and estimated 1RM per exercise per session (Epley over reps
plus reps in reserve). Tonnage is materialised into health_metrics as
strength_tonnage, so it can be correlated against sleep, HRV or readiness like
any other series.
- Get an API key from Hevy (Hevy Pro, developer settings).
- Enter it in FreeReps: Settings → Hevy, paste the key, set Sync from to the first date to import, click save. The key is verified against Hevy before it is stored.
- Sync runs every 30 minutes. Use "Sync now" for an immediate run; Settings → Import Logs carries the outcome.
The Sync from cutoff is what keeps a Hevy history and an imported Alpha Progression history from covering the same period twice — the training aggregates sum across sources without filtering on one.
Alpha Progression was the strength logger before Hevy, and its CSV export is still the way to bring that history in: exercises, sets, reps, weight and RIR. Its exercise names are mapped onto Hevy's catalog at ingest, so one exercise keeps one identity across both sources.
Upload it under Settings → Import, or POST it to /api/v1/ingest/alpha.
The export carries a bare wall clock with no time zone, and the resulting
instant is part of a row's natural key. ingest.session_timezone in
config.yaml decides how that clock is read, and it has to be the same on every
host that imports the same export — reading one export in two zones stores every
session twice (INCIDENTS.md, 2026-08-10).
124 metric names are on the allowlist. GET /api/v1/metrics/available returns
the list the running instance actually carries, with its display metadata; the
table below names the groups. Each user switches metrics off for their own
ingest in Settings → Ingest; a metric switched off is rejected from every
client and its stored rows are kept. The iOS app reads a subset of 38 types;
Health Auto Export sends what its automation selects.
| Category | Metrics |
|---|---|
| Cardiovascular | heart_rate, resting_heart_rate, heart_rate_variability, heart_rate_recovery_one_minute, blood_oxygen_saturation, respiratory_rate, vo2_max, blood_pressure_systolic, blood_pressure_diastolic, blood_pressure_heart_rate, atrial_fibrillation_burden |
| Sleep | sleep_analysis, apple_sleeping_wrist_temperature |
| Body | weight_body_mass, body_mass_index, body_fat_percentage, fat_mass, lean_body_mass, muscle_mass, bone_mass, body_water, height |
| Activity | active_energy, basal_energy_burned, step_count, flights_climbed, apple_exercise_time, apple_stand_time, apple_move_time, the four distance_* series |
| Strength | strength_tonnage — external load per session, derived from Hevy and Alpha sets |
| Oura | oura_readiness_score, oura_sleep_score, oura_activity_score, oura_temperature_deviation, oura_stress_high, oura_recovery_high, oura_resilience, oura_cardiovascular_age |
| Nutrition | 40 dietary_* series — macros, minerals, vitamins, caffeine, water |
| Clinical | blood_glucose, blood_alcohol_content, forced_vital_capacity, forced_expiratory_volume_1, electrodermal_activity |
| Environment | environmental_audio_exposure, headphone_audio_exposure |
| Cycling | cycling_power, cycling_cadence, cycling_speed, cycling_functional_threshold_power |
| Workouts | All types, with heart rate, routes and sets, deduped across sources |
Records that are not time series of a single number — ECG recordings, audiograms, medications, vision prescriptions, State of Mind entries and raw HealthKit category samples — are stored in their own tables and read through their own endpoints and MCP tools.
FreeReps exposes health data to Claude (and other LLMs) via the Model Context Protocol, over two transports:
- stdio, for a client that starts the binary itself.
- Streamable HTTP at
/mcp, served by the same HTTP server as the dashboard and behind the same Tailscale identity middleware, so each user sees only their own data.
There is no SSE endpoint. /mcp/sse matches no route, so the request reaches
the dashboard handler and the response is HTML — which a client reports as a
protocol error rather than as a wrong URL.
Tools (20):
| Group | Tools |
|---|---|
| Metrics | get_health_metrics, get_metric_stats, list_available_metrics, get_correlation, compare_periods |
| Sleep | get_sleep_data, get_sleep_summary |
| Workouts | get_workouts, get_workout_sets |
| Strength | get_strength_summary, get_strength_volume, get_strength_intensity, get_strength_1rm |
| Clinical records | get_ecg_recordings, get_audiograms, get_medications, get_vision_prescriptions |
| Other samples | get_activity_summaries, get_state_of_mind, get_category_samples |
Resources (3): daily_summary, recent_workouts, metric_catalog
The running server is the authority on this list — ask it rather than this table if the two disagree.
freereps --mcp -config config.yamlAdd to your Claude Code MCP config:
{
"mcpServers": {
"freereps": {
"command": "/path/to/freereps",
"args": ["--mcp", "-config", "/path/to/config.yaml"]
}
}
}/mcp speaks Streamable HTTP as soon as the server is running. A client that
can address an HTTP MCP endpoint needs nothing else — the URL is
https://freereps.your-tailnet.ts.net/mcp, and Tailscale authenticates the
request.
For a client that only speaks stdio, mcp-proxy bridges the two:
brew install mcp-proxy # or: pip install mcp-proxy{
"mcpServers": {
"freereps": {
"command": "mcp-proxy",
"args": ["--transport", "streamablehttp", "https://freereps.your-tailnet.ts.net/mcp"]
}
}
}No local FreeReps binary and no database access are needed on the client side.
An integration that stops delivering is the failure this project could not see: the container is healthy, the dashboard answers, and a source simply writes no more rows. FreeReps therefore reports that state itself, as a JSON POST to an ntfy topic — or to any endpoint that accepts one.
The conditions are evaluated from import_logs rather than from inside the sync
loops, so a syncer that stopped running is covered as well:
monitor_id |
Condition |
|---|---|
| 9200 | the manual test from Settings → Alerts |
| 9201 | the Withings sync failed the configured number of times in a row |
| 9202 | the Oura sync failed the configured number of times in a row |
| 9203 | the Hevy sync failed the configured number of times in a row |
| 9204 | no Apple Health metric stored for longer than the silence threshold |
| 9205 | no Apple Health workout delivered for longer than the silence threshold |
| 9206 / 9207 / 9208 | the Withings, Oura or Hevy sync keeps succeeding without storing a row for 7 days, 48 hours or 14 days |
The payload follows Uptime Kuma's webhook shape, so an existing Kuma consumer needs no second parser:
{
"schema": 1,
"monitor_id": 9201,
"service": "freereps - withings sync",
"status": 0,
"hostname": "freereps",
"monitor_type": "freereps",
"since": "2026-09-20T11:18:08Z",
"msg": "3 consecutive failed runs — user 2 since 2026-09-20T09:41:56Z: …"
}status is 0 for a problem and 1 for its resolution. Four rules shape what
arrives:
- One message per transition. The state per condition is stored
(
alert_state), so a problem is reported when it starts and again when it clears — not on every check cycle. - A resolution always follows. A condition that only ever sent
0would leave a stale alert in whatever reads the topic. - A threshold sits in front of the channel. The default is three consecutive failed runs of one source for one user, which at a 30-minute sync interval reports a real defect within two hours while a single DNS timeout stays out.
- The state is written after the send succeeded. An unreachable topic delays an alert; it does not swallow it.
Failures are counted per user, because a second user whose sync works would otherwise mask a first user whose sync does not.
Settings → Alerts: the topic URL, the name to report as, the check interval,
the failure threshold, the silence threshold, and a button that posts a test
message on 9200 with status: 1. The values live in the database; the alerts
block in config.yaml seeds them on first start only, so a redeploy cannot
overwrite what was entered in the UI.
alerts:
enabled: false
ntfy_url: "https://ntfy.example.com/freereps-alerts"
hostname: "freereps"
check_interval: "5m"
failure_threshold: 3
apple_silence: "36h" # 0 turns the Health Auto Export rule offReading the topic back is the quickest way to tell "FreeReps did not send" from "the subscriber did not receive":
curl -s "https://ntfy.example.com/freereps-alerts/json?poll=1&since=10m"A server seeded with generated data, for a first look, and the CLI that backfills Apple Health history into a running instance.
Run a FreeReps server with demo data, for a first look or for testing an ingest path against a database that carries no real measurements:
Using Docker (recommended)
cd FreeReps/server
cp config.example.yaml config.yaml
# Set tailscale.enabled: false in config.yaml for local dev
docker compose up -d db
docker compose run --rm -e FREEREPS_DEMO=true appFrom source
cd FreeReps/server
cp config.example.yaml config.yaml
# Set tailscale.enabled: false in config.yaml for local dev
docker compose up -d db
cd web && npm ci && npm run build && cd ..
go run ./cmd/freereps -config config.yaml -demoThis seeds the database with 90 days of generated health data — heart rate, sleep, workouts, activity rings and strength sessions with sets, reps and effort ratings. The data is deterministic and idempotent — restarting with -demo or FREEREPS_DEMO=true won't create duplicates.
The server will be available at http://localhost:8080. To tear down:
docker compose down -vfreereps-upload is a client-side CLI tool that brings historical
Health Auto Export data
into FreeReps. It runs in two modes:
- File mode (
-path) reads the.haefiles Health Auto Export writes to iCloud Drive, converts them to the REST format and posts them to the server. - TCP mode (
-hae-host) queries the app's own server connection over JSON-RPC and walks a date range in chunks, so a backfill needs no file export at all.
Install:
curl -sSL https://raw.githubusercontent.com/meltforce/FreeReps/main/server/scripts/install-upload.sh | bashUsage:
# First run — upload all historical data
freereps-upload \
-server https://freereps.your-tailnet.ts.net \
-path ~/Library/Mobile\ Documents/com~apple~CloudDocs/Health\ Auto\ Export/AutoSync
# Subsequent runs — only new/changed files are uploaded (resumable)
freereps-upload \
-server https://freereps.your-tailnet.ts.net \
-path ~/Library/Mobile\ Documents/com~apple~CloudDocs/Health\ Auto\ Export/AutoSyncFlags:
| Flag | Default | Description |
|---|---|---|
-server |
(required) | FreeReps server URL |
-path |
Path to AutoSync directory (or parent) — file mode | |
-batch-size |
2000 | Data points per metric payload (file mode) |
-hae-host |
IP address of the Health Auto Export TCP server — TCP mode | |
-hae-port |
9000 | Port of the Health Auto Export TCP server |
-start |
1 year ago | Start date of the backfill, yyyy-MM-dd (TCP mode) |
-end |
today | End date of the backfill, yyyy-MM-dd (TCP mode) |
-chunk-days |
1 | Days per query chunk (TCP mode) |
-dry-run |
false | Parse and convert without sending |
-version |
Print version and exit |
Requirements: lzfse must be installed for file mode (brew install lzfse).
Update / Uninstall:
# Update to latest version
curl -sSL https://raw.githubusercontent.com/meltforce/FreeReps/main/server/scripts/install-upload.sh | bash -s -- --update
# Uninstall
curl -sSL https://raw.githubusercontent.com/meltforce/FreeReps/main/server/scripts/install-upload.sh | bash -s -- --uninstallState tracking (file mode): Upload progress is tracked in ~/.freereps-upload/state.db (SQLite). Files are identified by path + size + SHA-256 hash, so changed files are re-uploaded and the tool is fully resumable.
Every route below sits behind the Tailscale identity middleware and answers for
the calling user only. /api/v1/version is the exception — it answers without
an identity, so a health check needs no credentials.
| Endpoint | Method | Description |
|---|---|---|
/api/v1/version |
GET | Build version (no identity required) |
/api/v1/me |
GET | Current user identity |
/api/v1/ingest/ |
POST | Ingest health data JSON (Health Auto Export REST; the iOS app with X-FreeReps-Client: freereps-ios) |
/api/v1/sync-state |
GET | Where the calling client's stored data ends, per domain (see server/specs/hae-rest-api.md) |
/api/v1/ingest/alpha |
POST | Ingest Alpha Progression CSV |
/api/v1/import |
POST | Unified import (auto-detects format) |
/api/v1/import/hae-tcp/check |
POST | Probe a Health Auto Export TCP server |
/api/v1/import/hae-tcp |
POST/DELETE | Start or cancel a TCP backfill |
/api/v1/import/hae-tcp/status |
GET | Progress of the running backfill |
/api/v1/import/hae-tcp/events |
GET | Progress as a server-sent event stream |
/api/v1/metrics/latest |
GET | Latest value per metric |
/api/v1/metrics |
GET | Time-range metric query |
/api/v1/metrics/stats |
GET | Metric statistics (avg, min, max, stddev) |
/api/v1/metrics/available |
GET | Available metrics with display metadata |
/api/v1/metrics/visibility |
PUT | Save per-user metric visibility |
/api/v1/timeseries |
GET | Time-bucketed metric data |
/api/v1/correlation |
GET | Pearson r between two metrics |
/api/v1/allowlist |
GET | Metric allowlist, enabled resolved for the calling user |
/api/v1/metrics/enabled |
PUT | Save per-user ingest enablement |
/api/v1/sleep |
GET | Sleep sessions + stages |
/api/v1/workouts |
GET | Workout list with filters |
/api/v1/workouts/zones |
GET | Heart rate zone distribution |
/api/v1/workouts/{id} |
GET | Workout detail |
/api/v1/workouts/{id}/sets |
GET | Strength sets of a session |
/api/v1/training-metrics/rebuild |
POST | Recompute the derived training series |
/api/v1/ecg |
GET | ECG recordings |
/api/v1/audiograms |
GET | Audiograms |
/api/v1/activity-summaries |
GET | Daily activity ring totals |
/api/v1/medications |
GET | Medication records |
/api/v1/vision-prescriptions |
GET | Vision prescriptions |
/api/v1/state-of-mind |
GET | State of Mind entries |
/api/v1/category-samples |
GET | HealthKit category samples |
/api/v1/preferences/front-page-heroes |
PUT | The four numbers on the front page |
/api/v1/preferences/max-heart-rate |
GET/PUT | Maximum heart rate for the zone split |
/api/v1/preferences/birth-date |
GET/PUT | Date of birth, used to estimate the maximum when none is set |
/api/v1/source-priority |
GET/PUT | Source priority configuration |
/api/v1/source-priority/{category} |
DELETE | Remove one category's rule |
/api/v1/oura/status |
GET | Oura connection status |
/api/v1/oura/credentials |
PUT | Save Oura OAuth2 credentials |
/api/v1/oura/authorize |
POST | Start Oura OAuth2 flow |
/api/v1/oura/sync |
POST | Trigger manual Oura sync |
/api/v1/oura/disconnect |
DELETE | Remove Oura connection |
/api/v1/withings/status |
GET | Withings connection status |
/api/v1/withings/credentials |
PUT | Save Withings OAuth2 credentials |
/api/v1/withings/authorize |
POST | Start Withings OAuth2 flow |
/api/v1/withings/sync |
POST | Trigger manual Withings sync |
/api/v1/withings/disconnect |
DELETE | Remove Withings connection |
/api/v1/hevy/status |
GET | Hevy connection status |
/api/v1/hevy/credentials |
PUT | Save the Hevy API key and sync cutoff |
/api/v1/hevy/sync |
POST | Trigger manual Hevy sync |
/api/v1/hevy/disconnect |
DELETE | Remove the Hevy API key |
/api/v1/alerts |
GET/PUT | Alert channel configuration and per-condition state |
/api/v1/alerts/test |
POST | Post a test message on monitor_id 9200 |
/api/v1/stats |
GET | Row counts and coverage per source |
/api/v1/import-logs |
GET | Recent ingest and sync runs |
/mcp |
POST/GET/DELETE | MCP over Streamable HTTP |
| File | Holds |
|---|---|
CLAUDE.md |
Conventions and gotchas for working in this repo. |
ROADMAP.md |
Open work. |
DECISIONS.md |
Decisions taken, with reasoning. |
INCIDENTS.md |
Postmortems. |
server/specs/ |
Wire formats and payload shapes of the ingest sources. |
app/README.md |
The iOS companion app. |
docs/mcp-server.md |
The MCP server in detail. |










