The Problem
Building a health product that supports multiple wearables means implementing separate OAuth flows, data mapping, and sync logic for each provider (Garmin, Whoop, Apple Health, Fitbit, etc.). Each provider has its own API, data formats, and units. Open Wearables consolidates that into one self-hosted API with normalized data models.
What This Does
Open Wearables is a full-stack platform: a FastAPI backend (backend/app/) that handles provider integrations, data normalization, and webhook delivery, plus a React/TanStack frontend (frontend/src/) for a developer portal. It supports Garmin, Oura, Strava, Apple Health, and others, with Celery task queues for background sync and webhook processing.
The system includes provider-specific webhook handlers (backend/app/services/providers/garmin/webhook_handler.py), a unified event model (backend/app/models/event_record.py), and an SDK for programmatic data ingestion. There's also an MCP server (mcp/app/main.py) for AI tool integration.
How It Is Wired
Execution starts in backend/app/main.py at _lifespan, which reaches 28 functions. It registers event types and ensures data sources exist, ending in a database write via db.execute. Webhook traffic enters through backend/app/services/providers/templates/base_webhook_handler.py at handle, which calls verify_signature and performs an HMAC check via hmac.new — two hops from entry to cryptographic verification.
The highest-traffic modules are the API route aggregator (backend/app/api/routes/v1/__init__.py, imports 31 modules) and the models/repositories/services __init__.py files, each importing 14–22 modules. These are pure aggregation points with instability 1.0 — change anything they re-export and everything downstream breaks.
The frontend has a genuine import cycle between frontend/src/routeTree.gen.ts and frontend/src/router.tsx. Both are in a circular dependency, so modifying routing structure requires touching both files in lockstep.
backend/tests/factories.py is the most-called file: 26 functions, 33 classes, imported by 73 other files. backend/app/utils/structured_logging.py (log_structured) is called from 113 places and 57 files — it's the logging backbone. backend/app/repositories/data_point_series_repository.py handles the heaviest data path: 23 functions, called from 45 files, with direct database writes.
How To Use It
git clone https://github.com/moses-y/open-wearables
cd open-wearables
docker compose up
Configuration lives in backend/config/.env.example (database, Redis, provider credentials) and frontend/.env.example (API URL, auth keys). The Makefile provides common targets. For local development: backend runs via uvicorn app.main:app from backend/, frontend via pnpm from frontend/. The docker-compose.yml handles the full stack including Celery workers and Redis.
Real-World Use
A fitness coaching app: create users via the API, send them connection links, and let them authorize Garmin or Whoop. The platform syncs data in the background (backend/app/integrations/celery/tasks/sync_vendor_data_task.py), normalizes it into event records, and your app queries GET /v1/events for heart rate, sleep, and activity data. Webhooks notify your backend when new data arrives.
Code Health & Issues
Static analysis (114 findings: 24 high, 89 medium, 1 low) found:
- High — Deep nesting in 36 files, worst in
backend/app/constants/workout_types/apple_xml.py(depth 8). Control flow is hard to follow; needs early returns or extraction. - High — 744 duplicated 6-line blocks across 161 files, notably in
backend/app/api/routes/v1/events.pyandsummaries.py. Shared helpers should replace them. - High — Import cycle between
frontend/src/routeTree.gen.tsandfrontend/src/router.tsx. - High — Oversized files:
frontend/src/lib/api/types.ts(661 lines),garmin/data_247.py(54 functions). - Medium — Broad exception handling in 10 files (
archival_task.py,seed_data_task.py,event_record_repository.py). - Medium — Hub modules:
frontend/src/lib/api/client.tshas 15 dependents; churn there is high-blast-radius.
Security audit: GitHub Actions use mutable tags (dorny/paths-filter@v3) instead of commit SHAs — pin them. Container base images (python:3.13-slim) aren't digest-pinned. No dependency vulnerability scan in CI. checkout keeps credentials for all steps; set persist-credentials: false. No job timeouts set. A generate_secrets.py script exists — verify it doesn't commit secrets.
The Bottom Line
A serious, well-structured platform with real provider integrations and a working frontend. The codebase is large and has genuine maintainability debt — duplicated route logic and deep nesting will slow changes. Worth adopting if you need multi-wearable support without building it yourself; budget time for the refactoring work.