Skip to main content

Core Interfaces

These interfaces define the contracts that health data providers must implement.

TokenProvider​

Manages OAuth tokens for health provider APIs. Implementations handle token storage, refresh, and expiration.

interface TokenProvider {
/**
* Get the current access token.
* May trigger a refresh if the token is expired.
*/
getAccessToken(): Promise<string>;

/**
* Get the refresh token, if available.
*/
getRefreshToken(): Promise<string | undefined>;

/**
* Update stored tokens after a refresh or new authentication.
* @param accessToken - New access token
* @param refreshToken - New refresh token (optional)
* @param expiresAt - Token expiration time (optional)
*/
updateTokens(
accessToken: string,
refreshToken?: string,
expiresAt?: Date
): Promise<void>;

/**
* Check if the current access token is expired.
*/
isExpired(): Promise<boolean>;
}

Implementation Notes​

  • getAccessToken() should automatically refresh expired tokens when possible
  • Store tokens securely (encrypted at rest, never logged)
  • expiresAt should include a buffer (e.g., 5 minutes before actual expiry)

Example Implementation​

import { TokenProvider } from '@wellpipe/core';

class DatabaseTokenProvider implements TokenProvider {
constructor(
private userId: string,
private db: Database,
private refreshFn: (refreshToken: string) => Promise<TokenResponse>
) {}

async getAccessToken(): Promise<string> {
const tokens = await this.db.getTokens(this.userId);

if (await this.isExpired()) {
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}

const newTokens = await this.refreshFn(refreshToken);
await this.updateTokens(
newTokens.accessToken,
newTokens.refreshToken,
new Date(Date.now() + newTokens.expiresIn * 1000)
);

return newTokens.accessToken;
}

return tokens.accessToken;
}

async getRefreshToken(): Promise<string | undefined> {
const tokens = await this.db.getTokens(this.userId);
return tokens?.refreshToken;
}

async updateTokens(
accessToken: string,
refreshToken?: string,
expiresAt?: Date
): Promise<void> {
await this.db.updateTokens(this.userId, {
accessToken,
refreshToken,
expiresAt,
});
}

async isExpired(): Promise<boolean> {
const tokens = await this.db.getTokens(this.userId);
if (!tokens?.expiresAt) return false;

// 5 minute buffer
const bufferMs = 5 * 60 * 1000;
return Date.now() >= tokens.expiresAt.getTime() - bufferMs;
}
}

HealthProvider​

Common interface for health data providers. Each provider (WHOOP, Oura, Garmin) implements this interface to normalize their API data.

interface HealthProvider {
/**
* Provider identifier (e.g., 'whoop', 'oura').
*/
readonly name: string;

/**
* Get sleep data for a date range.
*/
getSleep(startDate: string, endDate: string): Promise<SleepData[]>;

/**
* Get recovery data for a date range.
*/
getRecovery(startDate: string, endDate: string): Promise<RecoveryData[]>;

/**
* Get workout data for a date range.
*/
getWorkouts(startDate: string, endDate: string): Promise<WorkoutData[]>;
}

Date Format​

All date parameters use ISO 8601 format:

  • Date only: YYYY-MM-DD (e.g., 2024-01-15)
  • With time: YYYY-MM-DDTHH:mm:ss.sssZ (e.g., 2024-01-15T08:30:00.000Z)

Return Values​

  • Methods return empty arrays [] when no data exists for the date range
  • Data is sorted by date, oldest first
  • Each item includes the original providerId for traceability

Implementation Example​

import { HealthProvider, SleepData, RecoveryData, WorkoutData } from '@wellpipe/core';

class WhoopProvider implements HealthProvider {
readonly name = 'whoop';

constructor(private tokenProvider: TokenProvider) {}

async getSleep(startDate: string, endDate: string): Promise<SleepData[]> {
const token = await this.tokenProvider.getAccessToken();
const response = await fetch(
`https://api.prod.whoop.com/developer/v1/activity/sleep?start=${startDate}&end=${endDate}`,
{ headers: { Authorization: `Bearer ${token}` } }
);

const data = await response.json();
return data.records.map(this.normalizeSleep);
}

private normalizeSleep(whoopSleep: WhoopSleepRecord): SleepData {
return {
id: `whoop-${whoopSleep.id}`,
providerId: whoopSleep.id.toString(),
start: whoopSleep.start,
end: whoopSleep.end,
durationMs: whoopSleep.time_in_bed_milliseconds,
isNap: whoopSleep.nap,
score: whoopSleep.score?.sleep_performance_percentage,
stages: {
lightMs: whoopSleep.score?.stage_summary?.total_light_sleep_time_milli ?? 0,
deepMs: whoopSleep.score?.stage_summary?.total_slow_wave_sleep_time_milli ?? 0,
remMs: whoopSleep.score?.stage_summary?.total_rem_sleep_time_milli ?? 0,
awakeMs: whoopSleep.score?.stage_summary?.total_awake_time_milli ?? 0,
},
metrics: {
efficiency: whoopSleep.score?.sleep_efficiency_percentage,
respiratoryRate: whoopSleep.score?.respiratory_rate,
},
};
}

// Similar implementations for getRecovery and getWorkouts...
}

Design Decisions​

Why Promises?​

All methods return Promises because:

  1. Token refresh may be required before API calls
  2. Health APIs are remote and inherently async
  3. Allows for caching implementations

Why Date Strings?​

Date parameters are strings (not Date objects) because:

  1. JSON serialization is straightforward
  2. Avoids timezone conversion issues
  3. Matches how most health APIs accept dates

Why Separate Methods?​

We use getSleep(), getRecovery(), getWorkouts() instead of a single getData() because:

  1. Type safety - each method has a specific return type
  2. Providers may have different rate limits per endpoint
  3. Consumers often only need one data type