Data Sync & Background Jobs
Wearable data synchronization is managed through Junction Health (Vital API) integration, deployed as a Render.com cron job.
Junction Health Integration
Loop uses Junction Health (powered by Vital API) as a unified wearable data aggregator. This provides:
- Single API for 50+ wearable devices (Oura, Whoop, Apple Health, Fitbit, etc.)
- Automatic OAuth token management
- Standardized data format across all devices
- Historical data backfill support
Sync Jobs
wearable-sync-daily (Render.com Cron)
Schedule: Daily at 3:00 AM UTC
Platform: Render.com (auto-deployed from render.yaml)
Source: apps/background-jobs/wearable-sync
Syncs wearable data from Junction/Vital API to patient-graph database.
Process:
- Read user IDs from
JUNCTION_SYNC_USER_IDSenvironment variable - For each user:
- Fetch last 7 days of wearable data from Junction API (configurable via
JUNCTION_SYNC_LOOKBACK_DAYS) - Includes: sleep, activity, heart rate, recovery metrics
- Store raw readings in
patient_graph.patient_wearable_readings - Upsert daily aggregates in
patient_graph.patient_wearable_daily_stats
- Fetch last 7 days of wearable data from Junction API (configurable via
- Retry failed syncs up to 3 times with exponential backoff
- Log comprehensive sync summary (users synced, records stored, errors)
Configuration:
// apps/background-jobs/wearable-sync/index.ts
export async function runWearableSyncJob(config: {
junctionApiKey: string;
patientGraphApiUrl: string;
patientGraphApiKey: string;
userIds: string[];
lookbackDays?: number; // Default: 7
environment?: "production" | "sandbox";
region?: "us" | "eu";
}): Promise<Result<{ results: UserSyncSummary[] }>>Environment Variables:
JUNCTION_API_KEY- Vital API keyPATIENT_GRAPH_API_URL- Patient Graph API endpointPATIENT_GRAPH_API_KEY- API authentication keyJUNCTION_SYNC_USER_IDS- Comma-separated Vital user IDs (e.g.,user_123,user_456)JUNCTION_SYNC_LOOKBACK_DAYS- Days to sync (default: 7)JUNCTION_ENV-productionorsandboxJUNCTION_REGION-usoreu
Deployment:
# Automatically deployed on push to main via render.yaml
git push origin main
# Check deployment status
# Visit: https://dashboard.render.com → wearable-sync-dailysyncWhoopData
Schedule: Daily
Dedicated Whoop data sync job for recovery, strain, sleep, and workout data.
syncCgmReadings
Schedule: Every 30 minutes
Syncs CGM readings from Dexcom and Libre devices.
Process:
- Query all active CGM connections
- For each connection:
- Refresh OAuth tokens if needed
- Fetch glucose readings since
lastSyncAt - Store in
patient_glucose_readings - Update
lastSyncAton the connection record
aggregateWearableData
Schedule: Daily after sync jobs
Computes daily statistics from raw wearable readings:
- Average, min, max for each metric type
- Rolling 7-day and 30-day averages
- Stores in
patient_wearable_daily_stats
cleanupWearableData
Schedule: Weekly
Removes expired or redundant wearable data:
- Raw readings older than 90 days (daily stats are retained)
- Orphaned records from disconnected devices
- Duplicate readings
Manual Sync
Users can trigger a manual sync from the consumer app:
curl -X POST "https://my.loop.health/api/patient-graph/wearables/sync" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{ "source": "oura" }'Or for the generic wearable endpoint:
curl -X POST "https://my.loop.health/api/wearables/oura/sync" \
-H "Authorization: Bearer $CLERK_JWT"Error Handling
| Error | Handling |
|---|---|
| Token expired | Auto-refresh; if refresh fails, mark connection as inactive |
| Rate limited | Exponential backoff with retry |
| API unavailable | Retry up to 3 times, then skip user |
| Invalid data | Log warning, skip individual reading |
| Network timeout | 30-second timeout, retry |
Monitoring
Sync job health is monitored via:
- Trigger.dev dashboard — Job status, duration, failure rates
- Sentry — Error tracking and alerting
- Patient events —
wearable_syncevents for audit trail