πŸ“š Wearable Device Integration Advanced 22 min read

How to Integrate Wearable Device Data into EHR Systems with HL7 FHIR

Complete technical guide to integrating wearable device data from Apple HealthKit, Google Fit, and Fitbit into EHR systems using HL7 FHIR standards, OAuth authentication, and real-time data pipelines.

✍️
Dr. Alex Kumar

Introduction: The Wearable Integration Challenge

Integrating wearable device data into Electronic Health Record (EHR) systems presents unique technical challenges: disparate data formats across platforms, OAuth authentication complexities, real-time data streaming requirements, data validation and quality assurance, and HIPAA-compliant security measures. This comprehensive guide walks you through building a production-ready wearable integration system using HL7 FHIR standards.

Why Integrate Wearables with EHR Systems?

Healthcare organizations are rapidly adopting wearable device integration for several compelling reasons:

  • Enhanced Clinical Insights: Continuous monitoring data reveals patterns invisible in episodic clinic visits
  • Remote Patient Monitoring: Enable post-discharge surveillance and chronic disease management
  • Patient Engagement: Patients actively contributing health data feel more engaged in their care
  • Preventive Care: Early detection of deteriorating trends enables proactive interventions
  • Reimbursement: CPT codes 99453/99454/99457 provide revenue for RPM programs using wearables

However, building a robust integration system traditionally requires 6-12 months of development work. JustCopy.ai changes this entirely. With specialized AI agents, you can clone proven wearable integration templates and customize them to your exact requirements in days, not months.

System Architecture Overview

A complete wearable integration system consists of these components:

Architecture Diagram (Conceptual)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Wearable       β”‚
β”‚  Devices        β”‚
β”‚  - Apple Watch  β”‚
β”‚  - Fitbit       β”‚
β”‚  - Google Fit   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚                                 β”‚
         β–Ό                                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Platform APIs     β”‚          β”‚  OAuth 2.0       β”‚
β”‚  - HealthKit       β”‚          β”‚  Authentication  β”‚
β”‚  - Fitbit Web API  β”‚          β”‚  Server          β”‚
β”‚  - Google Fit      β”‚          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Integration       β”‚
β”‚  Middleware        β”‚
β”‚  - Data Retrieval  β”‚
β”‚  - Transformation  β”‚
β”‚  - Validation      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  FHIR Server       β”‚
β”‚  - Observation     β”‚
β”‚  - Device          β”‚
β”‚  - Patient         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  EHR System        β”‚
β”‚  - Epic            β”‚
β”‚  - Cerner          β”‚
β”‚  - Custom          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

JustCopy.ai provides pre-built templates for this entire architecture with all components integrated, tested, and ready to deploy.

Step 1: Set Up Development Environment

Technology Stack Selection

Backend:

  • Node.js (v18+) or Python (3.10+)
  • Express.js or FastAPI for REST APIs
  • PostgreSQL for structured data storage
  • Redis for caching and session management

Authentication & Security:

  • OAuth 2.0 client libraries
  • JWT for session tokens
  • TLS certificates for secure communication

FHIR Libraries:

  • Node.js: fhir-kit-client, @asymmetrik/node-fhir-server-core
  • Python: fhirclient, fhir.resources

Cloud Infrastructure:

  • AWS, Google Cloud, or Azure
  • Container orchestration (Docker, Kubernetes)
  • Message queues (RabbitMQ, AWS SQS) for async processing

Local Development Setup

# Create project structure
mkdir wearable-ehr-integration
cd wearable-ehr-integration

# Initialize Node.js project
npm init -y

# Install core dependencies
npm install express axios fhir-kit-client \
  passport passport-oauth2 jsonwebtoken \
  pg redis dotenv helmet cors

# Install development dependencies
npm install --save-dev typescript @types/node \
  @types/express jest supertest nodemon

# Create project structure
mkdir -p src/{routes,services,models,middleware,utils}
mkdir -p config tests

Or skip all setup and use JustCopy.ai: Simply select the wearable integration template, and the AI agents automatically provision your complete development environment with all dependencies pre-configured.

Step 2: Implement OAuth 2.0 Authentication

All major wearable platforms use OAuth 2.0 for user authorization. You must obtain access tokens to retrieve user health data.

Apple HealthKit OAuth Flow

Apple HealthKit uses a native iOS SDK rather than web OAuth. For server-side integration, you’ll use the Apple Health Records API:

Configuration:

  1. Register for Apple Developer Account
  2. Enable HealthKit capability in your iOS app
  3. Configure Health Records authorization

iOS Client Implementation:

// Swift: Request HealthKit authorization
import HealthKit

class HealthKitManager {
    let healthStore = HKHealthStore()

    func requestAuthorization(completion: @escaping (Bool, Error?) -> Void) {
        guard HKHealthStore.isHealthDataAvailable() else {
            completion(false, NSError(domain: "HealthKit not available", code: 1))
            return
        }

        // Define data types to read
        let readTypes: Set<HKObjectType> = [
            HKObjectType.quantityType(forIdentifier: .heartRate)!,
            HKObjectType.quantityType(forIdentifier: .stepCount)!,
            HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
            HKObjectType.quantityType(forIdentifier: .bodyMass)!,
            HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic)!,
            HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic)!,
            HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
        ]

        healthStore.requestAuthorization(toShare: nil, read: readTypes) { success, error in
            completion(success, error)
        }
    }

    func queryHeartRateData(
        startDate: Date,
        endDate: Date,
        completion: @escaping ([HKQuantitySample]?, Error?) -> Void
    ) {
        let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)!
        let predicate = HKQuery.predicateForSamples(
            withStart: startDate,
            end: endDate,
            options: .strictStartDate
        )

        let query = HKSampleQuery(
            sampleType: heartRateType,
            predicate: predicate,
            limit: HKObjectQueryNoLimit,
            sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: true)]
        ) { query, samples, error in
            guard let heartRateSamples = samples as? [HKQuantitySample] else {
                completion(nil, error)
                return
            }
            completion(heartRateSamples, nil)
        }

        healthStore.execute(query)
    }
}

Fitbit OAuth 2.0 Implementation

Step 1: Register Application

  • Create app at https://dev.fitbit.com/apps
  • Set OAuth 2.0 Application Type: β€œServer”
  • Define redirect URI: https://yourdomain.com/auth/fitbit/callback
  • Note your Client ID and Client Secret

Step 2: Implement Authorization Flow

// Node.js: Fitbit OAuth 2.0 server implementation
const express = require('express');
const axios = require('axios');
const router = express.Router();

const FITBIT_CLIENT_ID = process.env.FITBIT_CLIENT_ID;
const FITBIT_CLIENT_SECRET = process.env.FITBIT_CLIENT_SECRET;
const REDIRECT_URI = process.env.FITBIT_REDIRECT_URI;

// Step 1: Redirect user to Fitbit authorization page
router.get('/auth/fitbit', (req, res) => {
  const patientId = req.query.patientId; // Track which patient is authorizing

  const authUrl = 'https://www.fitbit.com/oauth2/authorize' +
    `?client_id=${FITBIT_CLIENT_ID}` +
    '&response_type=code' +
    `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
    '&scope=activity heartrate sleep weight nutrition profile' +
    `&state=${patientId}`; // Pass patient ID in state param

  res.redirect(authUrl);
});

// Step 2: Handle callback and exchange code for tokens
router.get('/auth/fitbit/callback', async (req, res) => {
  const { code, state: patientId } = req.query;

  try {
    // Exchange authorization code for access token
    const tokenResponse = await axios.post(
      'https://api.fitbit.com/oauth2/token',
      new URLSearchParams({
        client_id: FITBIT_CLIENT_ID,
        grant_type: 'authorization_code',
        redirect_uri: REDIRECT_URI,
        code: code
      }),
      {
        headers: {
          'Authorization': `Basic ${Buffer.from(
            `${FITBIT_CLIENT_ID}:${FITBIT_CLIENT_SECRET}`
          ).toString('base64')}`,
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      }
    );

    const {
      access_token,
      refresh_token,
      expires_in,
      user_id
    } = tokenResponse.data;

    // Store tokens securely in database (encrypted)
    await storeTokens({
      patientId: patientId,
      provider: 'fitbit',
      accessToken: access_token,
      refreshToken: refresh_token,
      expiresAt: new Date(Date.now() + expires_in * 1000),
      fitbitUserId: user_id
    });

    res.send('Fitbit authorization successful! You can close this window.');
  } catch (error) {
    console.error('Fitbit OAuth error:', error);
    res.status(500).send('Authorization failed');
  }
});

// Helper: Refresh expired access token
async function refreshFitbitToken(refreshToken) {
  const response = await axios.post(
    'https://api.fitbit.com/oauth2/token',
    new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken
    }),
    {
      headers: {
        'Authorization': `Basic ${Buffer.from(
          `${FITBIT_CLIENT_ID}:${FITBIT_CLIENT_SECRET}`
        ).toString('base64')}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  );

  return {
    accessToken: response.data.access_token,
    refreshToken: response.data.refresh_token,
    expiresAt: new Date(Date.now() + response.data.expires_in * 1000)
  };
}

module.exports = router;

Google Fit OAuth 2.0 Implementation

Step 1: Configure Google Cloud Project

Step 2: Implement Authorization

# Python: Google Fit OAuth 2.0 with Flask
from flask import Flask, request, redirect, session
from google_auth_oauthlib.flow import Flow
from google.oauth2.credentials import Credentials
import os

app = Flask(__name__)
app.secret_key = os.environ['FLASK_SECRET_KEY']

# OAuth 2.0 configuration
CLIENT_SECRETS_FILE = 'client_secret.json'
SCOPES = [
    'https://www.googleapis.com/auth/fitness.activity.read',
    'https://www.googleapis.com/auth/fitness.heart_rate.read',
    'https://www.googleapis.com/auth/fitness.sleep.read',
    'https://www.googleapis.com/auth/fitness.body.read'
]
REDIRECT_URI = 'https://yourdomain.com/auth/google/callback'

@app.route('/auth/google')
def auth_google():
    patient_id = request.args.get('patientId')
    session['patient_id'] = patient_id

    # Create OAuth flow
    flow = Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE,
        scopes=SCOPES,
        redirect_uri=REDIRECT_URI
    )

    authorization_url, state = flow.authorization_url(
        access_type='offline',
        include_granted_scopes='true',
        prompt='consent'
    )

    session['state'] = state
    return redirect(authorization_url)

@app.route('/auth/google/callback')
def auth_google_callback():
    state = session['state']
    patient_id = session['patient_id']

    # Complete OAuth flow
    flow = Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE,
        scopes=SCOPES,
        state=state,
        redirect_uri=REDIRECT_URI
    )

    flow.fetch_token(authorization_response=request.url)

    # Get credentials
    credentials = flow.credentials

    # Store tokens securely in database
    store_tokens({
        'patient_id': patient_id,
        'provider': 'google_fit',
        'access_token': credentials.token,
        'refresh_token': credentials.refresh_token,
        'token_uri': credentials.token_uri,
        'client_id': credentials.client_id,
        'client_secret': credentials.client_secret,
        'scopes': credentials.scopes,
        'expiry': credentials.expiry
    })

    return 'Google Fit authorization successful!'

JustCopy.ai’s OAuth templates include complete, battle-tested implementations for Apple HealthKit, Fitbit, Google Fit, Garmin, and 10+ other wearable platforms. The AI agents handle token storage, refresh logic, and secure encryption automatically.

Step 3: Fetch Data from Wearable APIs

Fitbit Data Retrieval

// Node.js: Fetch Fitbit data with automatic token refresh
class FitbitDataService {
  async getHeartRateData(patientId, date) {
    // Retrieve stored tokens
    const tokens = await getPatientTokens(patientId, 'fitbit');

    // Check if token expired
    if (new Date() >= tokens.expiresAt) {
      const newTokens = await refreshFitbitToken(tokens.refreshToken);
      await updateTokens(patientId, 'fitbit', newTokens);
      tokens.accessToken = newTokens.accessToken;
    }

    // Fetch heart rate data for specific date
    const url = `https://api.fitbit.com/1/user/-/activities/heart/date/${date}/1d/1min.json`;

    try {
      const response = await axios.get(url, {
        headers: {
          'Authorization': `Bearer ${tokens.accessToken}`
        }
      });

      return response.data;
    } catch (error) {
      if (error.response?.status === 401) {
        // Token invalid, refresh and retry
        const newTokens = await refreshFitbitToken(tokens.refreshToken);
        await updateTokens(patientId, 'fitbit', newTokens);
        return this.getHeartRateData(patientId, date); // Retry
      }
      throw error;
    }
  }

  async getActivityData(patientId, date) {
    const tokens = await getPatientTokens(patientId, 'fitbit');

    const url = `https://api.fitbit.com/1/user/-/activities/date/${date}.json`;

    const response = await axios.get(url, {
      headers: { 'Authorization': `Bearer ${tokens.accessToken}` }
    });

    return {
      steps: response.data.summary.steps,
      distance: response.data.summary.distances[0]?.distance || 0,
      floors: response.data.summary.floors,
      activeMinutes: response.data.summary.veryActiveMinutes +
                      response.data.summary.fairlyActiveMinutes,
      caloriesBurned: response.data.summary.caloriesOut
    };
  }

  async getSleepData(patientId, date) {
    const tokens = await getPatientTokens(patientId, 'fitbit');

    const url = `https://api.fitbit.com/1.2/user/-/sleep/date/${date}.json`;

    const response = await axios.get(url, {
      headers: { 'Authorization': `Bearer ${tokens.accessToken}` }
    });

    return response.data.sleep[0]; // Most recent sleep session
  }
}

Google Fit Data Retrieval

# Python: Fetch Google Fit data
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
import datetime

class GoogleFitDataService:
    def __init__(self, credentials_dict):
        self.credentials = Credentials(
            token=credentials_dict['access_token'],
            refresh_token=credentials_dict['refresh_token'],
            token_uri=credentials_dict['token_uri'],
            client_id=credentials_dict['client_id'],
            client_secret=credentials_dict['client_secret'],
            scopes=credentials_dict['scopes']
        )
        self.service = build('fitness', 'v1', credentials=self.credentials)

    def get_step_count(self, start_date, end_date):
        """Fetch aggregated step count"""
        start_time_millis = int(start_date.timestamp() * 1000)
        end_time_millis = int(end_date.timestamp() * 1000)

        body = {
            'aggregateBy': [{
                'dataTypeName': 'com.google.step_count.delta',
                'dataSourceId': 'derived:com.google.step_count.delta:com.google.android.gms:estimated_steps'
            }],
            'bucketByTime': {'durationMillis': 86400000},  # 1 day
            'startTimeMillis': start_time_millis,
            'endTimeMillis': end_time_millis
        }

        response = self.service.users().dataset().aggregate(
            userId='me',
            body=body
        ).execute()

        step_data = []
        for bucket in response.get('bucket', []):
            for dataset in bucket.get('dataset', []):
                for point in dataset.get('point', []):
                    step_count = point.get('value', [{}])[0].get('intVal', 0)
                    timestamp = int(bucket['startTimeMillis']) / 1000
                    step_data.append({
                        'timestamp': datetime.datetime.fromtimestamp(timestamp),
                        'steps': step_count
                    })

        return step_data

    def get_heart_rate(self, start_date, end_date):
        """Fetch heart rate data"""
        start_time_millis = int(start_date.timestamp() * 1000)
        end_time_millis = int(end_date.timestamp() * 1000)

        data_source = 'derived:com.google.heart_rate.bpm:com.google.android.gms:merge_heart_rate_bpm'

        response = self.service.users().dataSources().datasets().get(
            userId='me',
            dataSourceId=data_source,
            datasetId=f'{start_time_millis}000000-{end_time_millis}000000'
        ).execute()

        heart_rate_data = []
        for point in response.get('point', []):
            hr_value = point.get('value', [{}])[0].get('fpVal', 0)
            timestamp = int(point['startTimeNanos']) / 1000000000
            heart_rate_data.append({
                'timestamp': datetime.datetime.fromtimestamp(timestamp),
                'heart_rate': hr_value
            })

        return heart_rate_data

Step 4: Transform Data to FHIR Format

All wearable data must be transformed to HL7 FHIR Observation resources before EHR integration.

FHIR Transformation Service

// TypeScript: Transform wearable data to FHIR
import { Observation, Device, Quantity, CodeableConcept } from 'fhir/r4';

interface WearableDataPoint {
  timestamp: Date;
  value: number;
  unit: string;
  deviceName: string;
  deviceId: string;
}

class FHIRTransformationService {
  /**
   * Transform heart rate data to FHIR Observation
   */
  transformHeartRate(
    patientId: string,
    dataPoint: WearableDataPoint
  ): Observation {
    return {
      resourceType: 'Observation',
      status: 'final',
      category: [{
        coding: [{
          system: 'http://terminology.hl7.org/CodeSystem/observation-category',
          code: 'vital-signs',
          display: 'Vital Signs'
        }]
      }],
      code: {
        coding: [{
          system: 'http://loinc.org',
          code: '8867-4',
          display: 'Heart rate'
        }],
        text: 'Heart rate'
      },
      subject: {
        reference: `Patient/${patientId}`
      },
      effectiveDateTime: dataPoint.timestamp.toISOString(),
      issued: new Date().toISOString(),
      valueQuantity: {
        value: dataPoint.value,
        unit: 'beats/minute',
        system: 'http://unitsofmeasure.org',
        code: '/min'
      },
      device: {
        reference: `Device/${dataPoint.deviceId}`,
        display: dataPoint.deviceName
      },
      meta: {
        tag: [{
          system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue',
          code: 'PATMAS',
          display: 'Patient Measured'
        }]
      }
    };
  }

  /**
   * Transform step count to FHIR Observation
   */
  transformStepCount(
    patientId: string,
    dataPoint: WearableDataPoint
  ): Observation {
    return {
      resourceType: 'Observation',
      status: 'final',
      category: [{
        coding: [{
          system: 'http://terminology.hl7.org/CodeSystem/observation-category',
          code: 'activity',
          display: 'Activity'
        }]
      }],
      code: {
        coding: [{
          system: 'http://loinc.org',
          code: '41950-7',
          display: 'Number of steps in 24 hour Measured'
        }],
        text: 'Step count'
      },
      subject: {
        reference: `Patient/${patientId}`
      },
      effectiveDateTime: dataPoint.timestamp.toISOString(),
      valueQuantity: {
        value: dataPoint.value,
        unit: 'steps',
        system: 'http://unitsofmeasure.org',
        code: '{steps}'
      },
      device: {
        reference: `Device/${dataPoint.deviceId}`,
        display: dataPoint.deviceName
      }
    };
  }

  /**
   * Transform blood pressure to FHIR Observation with components
   */
  transformBloodPressure(
    patientId: string,
    systolic: number,
    diastolic: number,
    timestamp: Date,
    deviceInfo: { name: string; id: string }
  ): Observation {
    return {
      resourceType: 'Observation',
      status: 'final',
      category: [{
        coding: [{
          system: 'http://terminology.hl7.org/CodeSystem/observation-category',
          code: 'vital-signs'
        }]
      }],
      code: {
        coding: [{
          system: 'http://loinc.org',
          code: '85354-9',
          display: 'Blood pressure panel with all children optional'
        }]
      },
      subject: {
        reference: `Patient/${patientId}`
      },
      effectiveDateTime: timestamp.toISOString(),
      component: [
        {
          code: {
            coding: [{
              system: 'http://loinc.org',
              code: '8480-6',
              display: 'Systolic blood pressure'
            }]
          },
          valueQuantity: {
            value: systolic,
            unit: 'mmHg',
            system: 'http://unitsofmeasure.org',
            code: 'mm[Hg]'
          }
        },
        {
          code: {
            coding: [{
              system: 'http://loinc.org',
              code: '8462-4',
              display: 'Diastolic blood pressure'
            }]
          },
          valueQuantity: {
            value: diastolic,
            unit: 'mmHg',
            system: 'http://unitsofmeasure.org',
            code: 'mm[Hg]'
          }
        }
      ],
      device: {
        reference: `Device/${deviceInfo.id}`,
        display: deviceInfo.name
      }
    };
  }

  /**
   * Create FHIR Device resource for wearable
   */
  createDeviceResource(
    deviceId: string,
    deviceName: string,
    manufacturer: string,
    model: string,
    patientId: string
  ): Device {
    return {
      resourceType: 'Device',
      id: deviceId,
      identifier: [{
        system: 'urn:device:serial',
        value: deviceId
      }],
      type: {
        coding: [{
          system: 'http://snomed.info/sct',
          code: '706767009',
          display: 'Patient monitoring system'
        }]
      },
      manufacturer: manufacturer,
      deviceName: [{
        name: deviceName,
        type: 'model-name'
      }],
      modelNumber: model,
      patient: {
        reference: `Patient/${patientId}`
      }
    };
  }
}

LOINC Code Mapping

Use standard LOINC codes for wearable measurements:

MeasurementLOINC CodeDisplay Name
Heart Rate8867-4Heart rate
Step Count41950-7Number of steps in 24 hour Measured
Body Weight29463-7Body weight
Blood Pressure85354-9Blood pressure panel
Systolic BP8480-6Systolic blood pressure
Diastolic BP8462-4Diastolic blood pressure
SpO259408-5Oxygen saturation in Arterial blood by Pulse oximetry
Sleep Duration93832-4Sleep duration
Body Temperature8310-5Body temperature
Respiratory Rate9279-1Respiratory rate

Step 5: Implement Data Validation

Validate wearable data before sending to EHR to prevent garbage data entry:

// TypeScript: Data validation service
interface ValidationResult {
  valid: boolean;
  errors: string[];
  warnings: string[];
}

class WearableDataValidator {
  /**
   * Validate heart rate measurement
   */
  validateHeartRate(value: number, context?: { age?: number; activity?: string }): ValidationResult {
    const result: ValidationResult = {
      valid: true,
      errors: [],
      warnings: []
    };

    // Biologically plausible range
    if (value < 30 || value > 250) {
      result.valid = false;
      result.errors.push(`Heart rate ${value} outside plausible range (30-250 bpm)`);
    }

    // Resting heart rate warnings
    if (context?.activity === 'resting') {
      if (value < 40) {
        result.warnings.push('Bradycardia: Resting HR < 40 bpm');
      }
      if (value > 100) {
        result.warnings.push('Tachycardia: Resting HR > 100 bpm');
      }
    }

    // Age-adjusted maximum during exercise
    if (context?.age && context?.activity === 'exercise') {
      const maxHR = 220 - context.age;
      if (value > maxHR * 1.1) {
        result.warnings.push(`Heart rate exceeds age-adjusted maximum (${maxHR} bpm)`);
      }
    }

    return result;
  }

  /**
   * Validate step count
   */
  validateStepCount(value: number): ValidationResult {
    const result: ValidationResult = { valid: true, errors: [], warnings: [] };

    if (value < 0) {
      result.valid = false;
      result.errors.push('Step count cannot be negative');
    }

    if (value > 100000) {
      result.valid = false;
      result.errors.push('Step count exceeds plausible daily maximum (100,000)');
    }

    if (value > 50000) {
      result.warnings.push('Unusually high step count (>50,000)');
    }

    return result;
  }

  /**
   * Validate blood pressure
   */
  validateBloodPressure(systolic: number, diastolic: number): ValidationResult {
    const result: ValidationResult = { valid: true, errors: [], warnings: [] };

    // Plausibility checks
    if (systolic < 50 || systolic > 250) {
      result.valid = false;
      result.errors.push(`Systolic BP ${systolic} outside plausible range`);
    }

    if (diastolic < 30 || diastolic > 150) {
      result.valid = false;
      result.errors.push(`Diastolic BP ${diastolic} outside plausible range`);
    }

    if (systolic <= diastolic) {
      result.valid = false;
      result.errors.push('Systolic BP must be greater than diastolic BP');
    }

    // Clinical warnings
    if (systolic >= 180 || diastolic >= 120) {
      result.warnings.push('Hypertensive crisis: Immediate medical attention needed');
    } else if (systolic >= 140 || diastolic >= 90) {
      result.warnings.push('Hypertension: Stage 2');
    } else if (systolic >= 130 || diastolic >= 80) {
      result.warnings.push('Hypertension: Stage 1');
    }

    if (systolic < 90 || diastolic < 60) {
      result.warnings.push('Hypotension detected');
    }

    return result;
  }

  /**
   * Detect duplicate data submissions
   */
  async detectDuplicates(
    patientId: string,
    dataType: string,
    timestamp: Date,
    value: number
  ): Promise<boolean> {
    // Query database for existing observation with same:
    // - Patient ID
    // - Data type (LOINC code)
    // - Timestamp (within 1-minute window)
    // - Value (exact match)

    const existingObs = await db.observations.findOne({
      patientId,
      loincCode: getLoincCode(dataType),
      timestamp: {
        $gte: new Date(timestamp.getTime() - 60000),
        $lte: new Date(timestamp.getTime() + 60000)
      },
      value: value
    });

    return !!existingObs;
  }
}

JustCopy.ai’s validation templates include comprehensive validation rules for 50+ wearable data types, automatically flagging implausible values, detecting duplicates, and generating clinical alerts for critical findings.

Step 6: Post Data to FHIR Server

// TypeScript: FHIR client for posting observations
import Client from 'fhir-kit-client';

class FHIRClientService {
  private client: Client;

  constructor(fhirServerUrl: string, accessToken: string) {
    this.client = new Client({
      baseUrl: fhirServerUrl,
      customHeaders: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/fhir+json'
      }
    });
  }

  /**
   * Post FHIR Observation to EHR
   */
  async postObservation(observation: Observation): Promise<string> {
    try {
      const response = await this.client.create({
        resourceType: 'Observation',
        body: observation
      });

      return response.id;
    } catch (error) {
      console.error('Failed to post observation:', error);
      throw new Error(`FHIR API error: ${error.message}`);
    }
  }

  /**
   * Batch post multiple observations
   */
  async batchPostObservations(observations: Observation[]): Promise<void> {
    const bundle = {
      resourceType: 'Bundle',
      type: 'transaction',
      entry: observations.map(obs => ({
        request: {
          method: 'POST',
          url: 'Observation'
        },
        resource: obs
      }))
    };

    try {
      await this.client.batch({ body: bundle });
    } catch (error) {
      console.error('Batch post failed:', error);
      throw error;
    }
  }

  /**
   * Query existing observations for patient
   */
  async getPatientObservations(
    patientId: string,
    loincCode?: string,
    startDate?: Date,
    endDate?: Date
  ): Promise<Observation[]> {
    const searchParams: any = {
      patient: patientId
    };

    if (loincCode) {
      searchParams.code = `http://loinc.org|${loincCode}`;
    }

    if (startDate) {
      searchParams.date = `ge${startDate.toISOString()}`;
    }

    if (endDate) {
      searchParams.date = `${searchParams.date || ''},le${endDate.toISOString()}`;
    }

    const response = await this.client.search({
      resourceType: 'Observation',
      searchParams
    });

    return response.entry?.map((e: any) => e.resource) || [];
  }
}

Step 7: Build End-to-End Integration Pipeline

Tie all components together in a complete data pipeline:

// TypeScript: Complete wearable-to-EHR integration pipeline
class WearableIntegrationPipeline {
  private fitbitService: FitbitDataService;
  private transformer: FHIRTransformationService;
  private validator: WearableDataValidator;
  private fhirClient: FHIRClientService;

  async syncPatientData(patientId: string, date: string): Promise<void> {
    console.log(`Starting sync for patient ${patientId} on ${date}`);

    try {
      // 1. Fetch data from Fitbit
      const [heartRateData, activityData, sleepData] = await Promise.all([
        this.fitbitService.getHeartRateData(patientId, date),
        this.fitbitService.getActivityData(patientId, date),
        this.fitbitService.getSleepData(patientId, date)
      ]);

      const observations: Observation[] = [];

      // 2. Process heart rate data
      for (const hrPoint of heartRateData['activities-heart-intraday']?.dataset || []) {
        // Validate
        const validation = this.validator.validateHeartRate(hrPoint.value);
        if (!validation.valid) {
          console.error(`Invalid heart rate: ${validation.errors}`);
          continue;
        }

        // Check for duplicates
        const isDuplicate = await this.validator.detectDuplicates(
          patientId,
          'heart-rate',
          new Date(`${date}T${hrPoint.time}`),
          hrPoint.value
        );
        if (isDuplicate) continue;

        // Transform to FHIR
        const observation = this.transformer.transformHeartRate(patientId, {
          timestamp: new Date(`${date}T${hrPoint.time}`),
          value: hrPoint.value,
          unit: 'bpm',
          deviceName: 'Fitbit',
          deviceId: 'fitbit-12345'
        });

        observations.push(observation);
      }

      // 3. Process activity data
      const stepValidation = this.validator.validateStepCount(activityData.steps);
      if (stepValidation.valid) {
        observations.push(
          this.transformer.transformStepCount(patientId, {
            timestamp: new Date(date),
            value: activityData.steps,
            unit: 'steps',
            deviceName: 'Fitbit',
            deviceId: 'fitbit-12345'
          })
        );
      }

      // 4. Batch post to FHIR server
      if (observations.length > 0) {
        await this.fhirClient.batchPostObservations(observations);
        console.log(`Successfully posted ${observations.length} observations`);
      }

    } catch (error) {
      console.error(`Sync failed for patient ${patientId}:`, error);
      throw error;
    }
  }

  /**
   * Schedule automatic daily sync for all enrolled patients
   */
  async scheduleDailySync(): Promise<void> {
    const enrolledPatients = await db.wearableEnrollments.find({ active: true });

    for (const enrollment of enrolledPatients) {
      const yesterday = new Date();
      yesterday.setDate(yesterday.getDate() - 1);
      const dateStr = yesterday.toISOString().split('T')[0];

      try {
        await this.syncPatientData(enrollment.patientId, dateStr);
      } catch (error) {
        console.error(`Failed to sync patient ${enrollment.patientId}:`, error);
        // Continue with next patient
      }
    }
  }
}

// Schedule daily sync at 2 AM
const cron = require('node-cron');
const pipeline = new WearableIntegrationPipeline(/* dependencies */);

cron.schedule('0 2 * * *', () => {
  console.log('Starting daily wearable data sync...');
  pipeline.scheduleDailySync();
});

Step 8: Build Patient-Facing Interface

Create a patient portal where users authorize wearable access and view synced data:

// React: Patient wearable connection interface
import React, { useState, useEffect } from 'react';

interface ConnectedDevice {
  provider: 'fitbit' | 'apple' | 'google';
  connected: boolean;
  lastSync?: Date;
}

function WearableConnectionPage() {
  const [devices, setDevices] = useState<ConnectedDevice[]>([]);

  useEffect(() => {
    fetchConnectedDevices();
  }, []);

  const fetchConnectedDevices = async () => {
    const response = await fetch('/api/patient/wearables');
    const data = await response.json();
    setDevices(data);
  };

  const connectDevice = (provider: string) => {
    // Redirect to OAuth flow
    window.location.href = `/auth/${provider}?patientId=${getCurrentPatientId()}`;
  };

  const disconnectDevice = async (provider: string) => {
    await fetch(`/api/patient/wearables/${provider}`, {
      method: 'DELETE'
    });
    fetchConnectedDevices();
  };

  return (
    <div className="wearable-connection-page">
      <h2>Connect Your Wearable Devices</h2>
      <p>Share activity, heart rate, and health data with your care team</p>

      <div className="device-list">
        <DeviceCard
          name="Fitbit"
          icon="/images/fitbit-icon.png"
          connected={devices.find(d => d.provider === 'fitbit')?.connected}
          lastSync={devices.find(d => d.provider === 'fitbit')?.lastSync}
          onConnect={() => connectDevice('fitbit')}
          onDisconnect={() => disconnectDevice('fitbit')}
        />

        <DeviceCard
          name="Apple Health"
          icon="/images/apple-health-icon.png"
          connected={devices.find(d => d.provider === 'apple')?.connected}
          lastSync={devices.find(d => d.provider === 'apple')?.lastSync}
          onConnect={() => connectDevice('apple')}
          onDisconnect={() => disconnectDevice('apple')}
        />

        <DeviceCard
          name="Google Fit"
          icon="/images/google-fit-icon.png"
          connected={devices.find(d => d.provider === 'google')?.connected}
          lastSync={devices.find(d => d.provider === 'google')?.lastSync}
          onConnect={() => connectDevice('google')}
          onDisconnect={() => disconnectDevice('google')}
        />
      </div>
    </div>
  );
}

How JustCopy.ai Can Help

Building this entire wearable integration system from scratch requires 6-12 months of development, deep expertise in OAuth 2.0, FHIR standards, healthcare regulations, and significant infrastructure setup.

JustCopy.ai transforms this timeline from months to days. With 10 specialized AI agents, you can:

βœ… Clone a complete wearable integration platform with Apple HealthKit, Fitbit, Google Fit, and Garmin support βœ… Customize data validation rules, FHIR mappings, and sync schedules to your requirements βœ… Deploy to production with automated CI/CD pipelines and infrastructure provisioning βœ… Monitor with built-in observability, error tracking, and performance analytics βœ… Scale automatically based on user growth and data volume

JustCopy.ai’s wearable integration templates include:

  • Complete OAuth 2.0 implementations for 10+ platforms
  • FHIR transformation libraries with 50+ LOINC code mappings
  • Data validation engines with clinical rules
  • Patient portal with device connection UI
  • Clinician dashboard for reviewing wearable data
  • Automated sync scheduling and retry logic
  • HIPAA-compliant security (encryption, audit logs, access controls)
  • Integration with Epic, Cerner, and custom FHIR servers

Start with a proven template, customize it to your needs, and launch in days instead of months.

Conclusion

Integrating wearable device data into EHR systems via HL7 FHIR unlocks powerful capabilities for remote patient monitoring, chronic disease management, and preventive care. While the technical complexity is significantβ€”spanning OAuth authentication, multi-platform APIs, FHIR transformations, data validation, and HIPAA complianceβ€”modern tools dramatically accelerate development.

Ready to build your wearable integration platform? Start with JustCopy.ai and deploy a production-ready Apple HealthKit, Fitbit API, Google Fit, and FHIR integration system in days, not months.


Additional Resources

πŸš€

Build This with JustCopy.ai

Skip months of development with 10 specialized AI agents. JustCopy.ai can copy, customize, and deploy this application instantly. Our AI agents write code, run tests, handle deployment, and monitor your applicationβ€”all following healthcare industry best practices and HIPAA compliance standards.