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.
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:
- Register for Apple Developer Account
- Enable HealthKit capability in your iOS app
- 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
- Create project at https://console.cloud.google.com
- Enable Google Fitness API
- Create OAuth 2.0 credentials (Web application)
- Add authorized redirect URI
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:
Measurement | LOINC Code | Display Name |
---|---|---|
Heart Rate | 8867-4 | Heart rate |
Step Count | 41950-7 | Number of steps in 24 hour Measured |
Body Weight | 29463-7 | Body weight |
Blood Pressure | 85354-9 | Blood pressure panel |
Systolic BP | 8480-6 | Systolic blood pressure |
Diastolic BP | 8462-4 | Diastolic blood pressure |
SpO2 | 59408-5 | Oxygen saturation in Arterial blood by Pulse oximetry |
Sleep Duration | 93832-4 | Sleep duration |
Body Temperature | 8310-5 | Body temperature |
Respiratory Rate | 9279-1 | Respiratory 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.