CPAP Integration

Health Observability integrates with ResMed MyAir to pull nightly CPAP/sleep data. Because MyAir uses a proprietary authentication scheme (including email-based MFA), the integration is handled by a separate Cloudflare Worker (myair-sync) that owns credentials and scheduling. The main Health Observability Worker is read-only with respect to CPAP data.

Architecture

graph TD subgraph Frontend["Browser (app.js)"] CPAP_Tab["CPAP Tab UI"] Settings_Auth["Settings → MyAir Auth"] end subgraph MainWorker["health-observability Worker"] D1[("D1 database\ncpap_data table")] end subgraph MyAirWorker["myair-sync Worker\nmyair-sync.eddie-547.workers.dev"] AuthAPI["POST /api/auth/start\nPOST /api/auth/verify\nGET /api/auth/status"] SyncAPI["POST /api/sync-now\nScheduled trigger (8am)"] DataAPI["GET /api/cpap\nGET /api/cpap-correlation"] MyAirPy["myair-py library\n(ResMed API client)"] end ResMed["ResMed MyAir API"] CPAP_Tab -->|"GET /api/cpap\nGET /api/cpap-correlation"| DataAPI CPAP_Tab -->|"POST /api/sync-now"| SyncAPI Settings_Auth -->|"Auth flow"| AuthAPI SyncAPI --> MyAirPy MyAirPy --> ResMed ResMed --> MyAirPy MyAirPy --> D1 DataAPI --> D1

Note: The myair-sync worker is a separate deployment. The health-observability D1 database is shared between both workers via the same binding (or the CPAP data is stored in the sync worker’s own storage — the exact sharing mechanism is internal to the sync worker’s implementation).

Authentication Flow

ResMed MyAir requires email-based multi-factor authentication. The flow is managed entirely in the myair-sync worker and exposed via its auth endpoints.

sequenceDiagram participant U as User participant UI as Browser UI participant SW as myair-sync Worker participant RM as ResMed MyAir U->>UI: Click "Send code" (Settings tab) UI->>SW: POST /api/auth/start SW->>RM: Initiate login RM-->>SW: MFA code sent to email SW-->>UI: { mfa_required: true } UI->>UI: Show Step 2 (code input) U->>UI: Enter email code, click "Verify" UI->>SW: POST /api/auth/verify { code } SW->>RM: Submit MFA code RM-->>SW: Session token / cookies SW-->>UI: { success: true } UI->>UI: Hide Step 2, show "Authenticated ✓" Note over SW: Stores session credentials securely Note over SW: Subsequent syncs use stored session

Auth Status States

Stateneeds_reauthlast_syncBadge
Authenticated + syncedfalsetimestampActive (green)
Session expiredtrueanyRe-auth needed (red)
Authenticated, never syncedfalsenullPending first sync (amber)
Worker unreachableUnavailable (red)

Sync Schedule

Data is synchronised from ResMed MyAir on two triggers:

  1. Scheduled sync — automatically at 8:00am daily (configured as a Cron Trigger in the myair-sync worker)
  2. Manual sync — the user clicks “Sync now” in the CPAP tab, triggering POST /api/sync-now
flowchart TD Trigger["Sync triggered\n(8am cron OR manual POST)"] Auth{"Session\nvalid?"} Auth -->|No| NeedsReauth["Set needs_reauth flag\nReturn error"] Auth -->|Yes| Fetch["Fetch today's CPAP data\nfrom ResMed MyAir"] Fetch --> Parse["Parse sleep metrics\n(AHI, usage, leak, score, pressure)"] Parse --> Upsert["UPSERT into cpap_data\nWHERE log_date = today"] Upsert --> Return["Return { synced: N }"]

The sync endpoint returns { synced: N } where N is the count of records written. The UI displays this in the status area after a manual sync.

Data Model

CPAP data is stored in the cpap_data table (Migration 08). Each row represents one night of sleep:

ColumnTypeDescription
log_dateTEXT (PK)Sleep date YYYY-MM-DD
ahiREALApnea-Hypopnea Index (events/hour)
usage_minutesINTEGERMinutes of mask usage
mask_leak_pctREALLarge leak percentage of total session
myair_scoreINTEGERResMed composite score 0–100
pressure_minREALMinimum delivered pressure (cmH₂O)
pressure_maxREALMaximum delivered pressure (cmH₂O)
synced_atDATETIMETimestamp of last upsert

log_date is the primary key, so nightly re-syncs are idempotent — the row is overwritten with the latest values.

CPAP API Endpoints (myair-sync Worker)

The frontend communicates with the following endpoints on the myair-sync worker:

GET /api/auth/status

Returns current authentication and sync status.

Response:

{
  "needs_reauth": false,
  "last_sync": "2026-03-22T08:05:00Z"
}

POST /api/auth/start

Initiates the ResMed MyAir login flow. ResMed sends an MFA code to the registered email address.

Response (MFA required):

{ "mfa_required": true }

Response (no MFA needed):

{ "success": true }

POST /api/auth/verify

Submits the MFA code to complete authentication.

Request:

{ "code": "123456" }

Response:

{ "success": true }

POST /api/sync-now

Triggers an immediate CPAP data sync.

Response:

{ "synced": 1 }

GET /api/cpap

Returns stored CPAP records, most recent first.

Query Parameters: limit (default 30)

Response:

{
  "entries": [
    {
      "log_date": "2026-03-21",
      "ahi": 2.1,
      "usage_minutes": 427,
      "mask_leak_pct": 3.2,
      "myair_score": 88,
      "pressure_min": 8.0,
      "pressure_max": 12.4,
      "synced_at": "2026-03-22T08:05:00Z"
    }
  ]
}

GET /api/cpap-correlation

Returns CPAP nights joined to the next morning’s blood pressure reading.

Response:

{
  "entries": [
    {
      "log_date": "2026-03-21",
      "myair_score": 88,
      "ahi": 2.1,
      "usage_minutes": 427,
      "mask_leak_pct": 3.2,
      "systolic": 122,
      "diastolic": 76
    }
  ]
}

Entries where no morning BP reading exists will have systolic: null and diastolic: null. The frontend filters these out for the correlation chart (entries.filter(e => e.systolic)).

Python Integration (myair-py)

The cf-requirements.txt file declares myair-py==0.1.2e, a Python package that interfaces with the ResMed MyAir API. This dependency is used by the myair-sync worker’s Python layer (or is used in a separate data pipeline). The main health-observability JavaScript codebase does not directly depend on it.

cf-requirements.txt
└── myair-py==0.1.2e   # ResMed MyAir API client