Fitness Tracker Integration Strategy
Your database is already well-architected for fitness trackers with a provider-agnostic design.
Decision
Choice
Priority providers
Fitbit → Whoop → Oura
Sync architecture
Hybrid (Edge Functions for OAuth providers, native SDKs for Apple/Google later)
Frontend UX
Profile screen expansion with "Connected Devices" section
Sync triggers
Manual, Scheduled (cron), Webhooks
Intraday data retention
Retain all data indefinitely
Initial backfill
Maximum available historical data
Cron schedule
Daily at 4 AM UTC (fixed time)
Token storage
Dedicated table per provider
Part 1: Backend Architecture
1.1 Current Schema (Already Solid)
fitness_trackers (provider-agnostic registry)
├── activities (workouts)
├── daily_summary (steps, calories, resting HR)
├── intraday_data (minute-level HR, step deltas)
└── sleep_sessions (sleep periods)
Key design strengths:
vendor_metadata JSONB columns preserve provider-specific data without schema changes
last_summary_sync_date / last_intraday_sync_date enable incremental sync
ingestion_log table ready for sync status tracking
1.2 Data Normalization Strategy
Normalized Fields
Provider-Specific (vendor_metadata)
steps, calories, distance_m
Fitbit: activityCalories, sedentaryMinutes
heart_rate, resting_heart_rate
Whoop: strain, recovery, hrv
total_duration_minutes (sleep)
Oura: readinessScore, temperatureDelta
activity_type, duration_sec
All: raw API response fields
Store raw API responses in vendor_metadata and extract key fields into normalized columns for cross-provider analytics.
1.3 Token Storage Architecture
Decision: Dedicated table per provider
Provider
Token Table
Fitbit
fitbit_tokens (already exists)
Whoop
whoop_tokens (to create)
Oura
oura_tokens (to create)
Reserve user_credentials for API keys or non-OAuth credentials.
Decision: Hybrid Approach
Provider
Integration Method
Fitbit
Edge Function (OAuth + REST API)
Whoop
Edge Function (OAuth + REST API)
Oura
Edge Function (OAuth + REST API)
Apple Health
Mobile HealthKit SDK (future)
Google Fit
Mobile Health Connect SDK (future)
Mobile App → Supabase Edge Function → Provider API → Supabase DB
Why Edge Functions for Fitbit/Whoop/Oura:
Tokens never leave your backend
Server-side token refresh (no mobile token expiry issues)
Works when app is closed (webhooks/scheduled syncs)
Single codebase for web + mobile
Part 2: OAuth Flow Architecture
2.1 Reuse Your WebView Pattern
Your GoogleAuthWebView.jsx pattern is reusable:
// TrackerAuthWebView.jsx
const authUrls = {
fitbit : `${ baseUrl } /api/auth/fitbit/start` ,
whoop : `${ baseUrl } /api/auth/whoop/start` ,
oura : `${ baseUrl } /api/auth/oura/start` ,
} ;
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Mobile App │────▶│ WebView Auth │────▶│ Provider │────▶│ Supabase │
│ │ │ Page │ │ OAuth │ │ Edge Fn │
└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘
│ │
│◀───────────── postMessage({ tracker_id, provider }) ──────┘
│
▼
Save tracker_id locally, start sync
2.3 Supabase Edge Function for OAuth Callback
// supabase/functions/fitbit-callback/index.ts
export default async function handler ( req : Request ) {
const { code, state } = parseQuery ( req . url ) ;
const userId = verifyState ( state ) ; // CSRF protection
// Exchange code for tokens
const tokens = await exchangeFitbitCode ( code ) ;
// Store tokens
await supabase . from ( 'fitbit_tokens' ) . upsert ( {
user_id : userId ,
access_token : tokens . access_token ,
refresh_token : tokens . refresh_token ,
expires_at : new Date ( Date . now ( ) + tokens . expires_in * 1000 ) ,
fitbit_user_id : tokens . user_id ,
scope : tokens . scope ,
} ) ;
// Create tracker record
const { data : tracker } = await supabase . from ( 'fitness_trackers' ) . insert ( {
user_id : userId ,
provider : 'fitbit' ,
device_name : 'Fitbit' , // Can be updated later with device info
} ) . select ( ) . single ( ) ;
// Return HTML that sends postMessage to WebView
return new Response ( successHtml ( tracker . tracker_id ) , {
headers : { 'Content-Type' : 'text/html' } ,
} ) ;
}
Part 3: Data Sync Strategy
Decision: Manual + Cron + Webhooks (no auto-sync on app foreground)
Trigger
Implementation
Manual
User taps "Sync Now" → calls Edge Function
Scheduled (cron)
Supabase pg_cron runs daily sync for all users
Webhooks
Provider pushes updates → Edge Function receives
3.2 Initial Backfill vs. Ongoing Sync
Sync Type
Trigger
Data Range
Initial backfill
After OAuth complete
Maximum available (all historical data)
Daily sync
Cron job (4 AM UTC)
Yesterday + today
Real-time
Webhooks
As received
3.3 Fitbit Sync Implementation
// supabase/functions/sync-fitbit/index.ts
export default async function handler ( req : Request ) {
const { user_id, tracker_id, sync_type } = await req . json ( ) ;
// Get fresh tokens (refresh if needed)
const tokens = await getFreshFitbitTokens ( user_id ) ;
// Determine date range (max available on initial, incremental thereafter)
const lastSync = await getLastSyncDate ( tracker_id , sync_type ) ;
const startDate = lastSync || null ; // null = fetch all available history
const endDate = new Date ( ) ;
// Fetch and store data
if ( sync_type === 'summary' ) {
await syncDailySummary ( tracker_id , tokens , startDate , endDate ) ;
} else if ( sync_type === 'intraday' ) {
await syncIntradayData ( tracker_id , tokens , startDate , endDate ) ;
} else if ( sync_type === 'sleep' ) {
await syncSleepSessions ( tracker_id , tokens , startDate , endDate ) ;
} else if ( sync_type === 'activities' ) {
await syncActivities ( tracker_id , tokens , startDate , endDate ) ;
}
// Log sync status
await supabase . from ( 'ingestion_log' ) . insert ( {
user_id,
provider : 'fitbit' ,
status : 'success' ,
message : `Synced ${ sync_type } from ${ startDate } to ${ endDate } ` ,
} ) ;
}
3.4 Data Transformer Pattern
// Normalize Fitbit data to your schema
function transformFitbitDailySummary ( fitbitData : FitbitSummary ) : DailySummaryInsert {
return {
tracker_id : trackerId ,
activity_date : fitbitData . dateTime ,
steps : fitbitData . summary . steps ,
distance_m : fitbitData . summary . distances . find ( d => d . activity === 'total' ) ?. distance * 1000 ,
calories_total : fitbitData . summary . caloriesOut ,
resting_heart_rate : fitbitData . summary . restingHeartRate ,
vendor_metadata : {
// Preserve Fitbit-specific data
activityCalories : fitbitData . summary . activityCalories ,
sedentaryMinutes : fitbitData . summary . sedentaryMinutes ,
lightlyActiveMinutes : fitbitData . summary . lightlyActiveMinutes ,
fairlyActiveMinutes : fitbitData . summary . fairlyActiveMinutes ,
veryActiveMinutes : fitbitData . summary . veryActiveMinutes ,
floors : fitbitData . summary . floors ,
elevation : fitbitData . summary . elevation ,
} ,
} ;
}
3.5 Intraday Data Retention
Decision: Retain all data indefinitely
Fitbit intraday HR data can be ~1440 rows/day/user. With full retention:
1 year = ~525,600 rows per user
Storage cost is acceptable for the analytical value
No retention policy needed
Part 4: Frontend UX Strategy
4.1 Profile Screen Enhancement
Add a "Connected Devices" section to your profile screen:
┌─────────────────────────────────────┐
│ Profile │
├─────────────────────────────────────┤
│ [Avatar] John Doe │
│ john@example.com │
├─────────────────────────────────────┤
│ Connected Devices │
│ ┌─────────────────────────────────┐ │
│ │ [+] Connect a Fitness Tracker │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ [✓] Fitbit Charge 5 │ │
│ │ Last synced: 2 hours ago │ │
│ │ [Sync Now] [Disconnect] │ │
│ └─────────────────────────────────┘ │
├─────────────────────────────────────┤
│ My Templates │
│ Account Settings │
│ ... │
└─────────────────────────────────────┘
4.2 Device Connection Flow
┌──────────────────────┐
│ Connect a Device │
├──────────────────────┤
│ [Fitbit logo] │
│ Fitbit │
├──────────────────────┤
│ [Whoop logo] │
│ WHOOP │
├──────────────────────┤
│ [Oura logo] │
│ Oura Ring │
└──────────────────────┘
mobile/src/
├── components/
│ ├── FitnessTracker/
│ │ ├── ConnectedDeviceCard.jsx # Shows connected tracker status
│ │ ├── DevicePickerModal.jsx # Provider selection grid
│ │ └── TrackerAuthWebView.jsx # OAuth WebView (reuse pattern)
│ └── ...
├── utils/
│ ├── fitnessTrackers/
│ │ ├── useConnectedTrackers.js # React Query hook for trackers
│ │ ├── useSyncTracker.js # Mutation hook for manual sync
│ │ └── providers.js # Provider config (logos, names, auth URLs)
│ └── ...
└── app/
└── (tabs)/
└── profile.jsx # Add Connected Devices section
Part 5: Provider-Specific Details
Aspect
Details
OAuth
Standard OAuth 2.0, 8-hour access token
API
REST, well-documented
Rate limits
150 requests/hour per user
Webhooks
Subscription API for real-time updates
Data available
Activities, sleep, HR (intraday requires app approval)
Token table
fitbit_tokens (exists)
Aspect
Details
OAuth
OAuth 2.0
API
REST API (newer, evolving)
Rate limits
TBD (generally permissive)
Webhooks
Available
Data available
Strain, recovery, sleep, HR, HRV
Token table
whoop_tokens (to create)
Whoop-specific vendor_metadata:
{
"strain" : 14.2 ,
"recovery" : 67 ,
"hrv" : 45 ,
"respiratory_rate" : 15.2 ,
"skin_temp" : 33.1
}
Aspect
Details
OAuth
OAuth 2.0
API
REST v2, straightforward
Rate limits
Generous (5000/day)
Webhooks
Available
Data available
Sleep, readiness, activity, HR
Token table
oura_tokens (to create)
Oura-specific vendor_metadata:
{
"readiness_score" : 85 ,
"sleep_score" : 78 ,
"activity_score" : 92 ,
"temperature_delta" : 0.2 ,
"lowest_heart_rate" : 52
}
Part 6: Implementation Phases
Phase 1: Foundation + Fitbit
Database:
Verify fitness tables exist in Supabase (run migrations if needed)
Add RLS policies for fitness tables
Frontend:
Build ConnectedDeviceCard component
Build DevicePickerModal component
Build TrackerAuthWebView component
Add "Connected Devices" section to profile screen
Create useConnectedTrackers React Query hook
Create useSyncTracker mutation hook
Backend (Edge Functions):
fitbit-auth-start - Initiates OAuth flow
fitbit-callback - Handles OAuth callback, stores tokens
sync-fitbit - Fetches and stores Fitbit data
fitbit-webhook - Receives real-time updates
Cron:
Set up pg_cron job for daily Fitbit sync
Phase 2: Whoop Integration
Database:
Create whoop_tokens table (mirror fitbit_tokens structure)
Backend (Edge Functions):
whoop-auth-start - Initiates OAuth flow
whoop-callback - Handles OAuth callback
sync-whoop - Fetches and stores Whoop data
whoop-webhook - Receives real-time updates
Frontend:
Add Whoop to DevicePickerModal
Update providers.js with Whoop config
Phase 3: Oura Integration
Database:
Backend (Edge Functions):
oura-auth-start - Initiates OAuth flow
oura-callback - Handles OAuth callback
sync-oura - Fetches and stores Oura data
oura-webhook - Receives real-time updates
Frontend:
Add Oura to DevicePickerModal
Update providers.js with Oura config
Phase 4: Analytics & Insights (Future)
Cross-provider data views (unified sleep analysis)
Correlate fitness data with food/supplement logging
Surface insights ("Your HRV improved 12% since starting creatine")
Compare with similar users (age, activity level, goals)
Phase 5: Additional Providers (Future)
Apple Health (HealthKit SDK - iOS native)
Google Fit (Health Connect SDK - Android native)
Garmin (OAuth 1.0a - more complex)
Part 7: Database Migrations Needed
-- whoop_tokens
CREATE TABLE whoop_tokens (
user_id UUID PRIMARY KEY REFERENCES user_profiles(user_id),
whoop_user_id TEXT ,
access_token TEXT NOT NULL ,
refresh_token TEXT NOT NULL ,
expires_at TIMESTAMPTZ NOT NULL ,
token_type TEXT ,
scope TEXT ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- oura_tokens
CREATE TABLE oura_tokens (
user_id UUID PRIMARY KEY REFERENCES user_profiles(user_id),
oura_user_id TEXT ,
access_token TEXT NOT NULL ,
refresh_token TEXT NOT NULL ,
expires_at TIMESTAMPTZ NOT NULL ,
token_type TEXT ,
scope TEXT ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- fitness_trackers RLS
ALTER TABLE fitness_trackers ENABLE ROW LEVEL SECURITY;
CREATE POLICY " Users can view own trackers"
ON fitness_trackers FOR SELECT
USING (auth .uid () = user_id);
CREATE POLICY " Users can insert own trackers"
ON fitness_trackers FOR INSERT
WITH CHECK (auth .uid () = user_id);
CREATE POLICY " Users can delete own trackers"
ON fitness_trackers FOR DELETE
USING (auth .uid () = user_id);
-- Similar policies for daily_summary, intraday_data, sleep_sessions, activities
Part 8: Architecture Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ MOBILE APP │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────────┐ │
│ │ Profile │ │ Device │ │ TrackerAuth │ │
│ │ Screen │──│ Picker │──│ WebView │ │
│ └─────────────┘ └──────────────┘ └─────────────────────────────┘ │
│ │ │ │
└─────────┼────────────────────────────────────────┼───────────────────┘
│ React Query │ OAuth WebView
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ SUPABASE EDGE FUNCTIONS │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────────┐ │
│ │ *-auth- │ │ sync-* │ │ *-webhook │ │
│ │ start │ │ (fitbit, │ │ (real-time │ │
│ │ *-callback │ │ whoop, oura) │ │ updates) │ │
│ └─────────────┘ └──────────────┘ └─────────────────────────────┘ │
│ │ │ ▲ │
│ │ │ │ Webhooks │
│ ▼ ▼ │ │
│ ┌──────────────────────────────────────────────┴────────────────┐ │
│ │ PROVIDER APIs │ │
│ │ Fitbit API | Whoop API | Oura API │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ SUPABASE DATABASE │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────────┐ │
│ │ fitness_ │ │ daily_ │ │ intraday_data │ │
│ │ trackers │──│ summary │ │ sleep_sessions │ │
│ │ │ │ activities │ │ fitbit/whoop/oura_tokens │ │
│ └─────────────┘ └──────────────┘ └─────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ pg_cron: Daily sync job for all connected trackers │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Part 9: Test Cases (TDD Approach)
9.1 Frontend Component Tests
ConnectedDeviceCard.test.jsx
Test Case
Description
renders device name and provider
Shows "Fitbit Charge 5" and Fitbit logo
renders last synced time
Shows "Last synced: 2 hours ago" with correct relative time
renders never synced state
Shows "Never synced" when last_synced is null
sync button triggers mutation
Tapping "Sync Now" calls useSyncTracker mutation
shows syncing state
Button shows spinner and "Syncing..." during sync
disconnect shows confirmation
Tapping "Disconnect" opens confirmation dialog
disconnect confirmed removes tracker
Confirming disconnect calls delete mutation
handles sync error
Shows error toast when sync fails
DevicePickerModal.test.jsx
Test Case
Description
renders when visible
Modal appears when visible=true
hidden when not visible
Modal hidden when visible=false
shows all supported providers
Lists Fitbit, Whoop, Oura with logos
provider tap triggers onSelect
Tapping Fitbit calls onSelect('fitbit')
close button dismisses
Tapping X calls onClose
disables already connected providers
Fitbit shown as "Connected" if already linked
shows provider descriptions
Each provider shows brief description
TrackerAuthWebView.test.jsx
Test Case
Description
renders WebView with correct URL
WebView source is fitbit auth URL for provider='fitbit'
handles success postMessage
Receiving {tracker_id, provider} calls onSuccess
handles error postMessage
Receiving {error} calls onError
close button dismisses
Tapping X calls onClose
shows loading indicator
Shows spinner while WebView loads
Profile Connected Devices Section
Test Case
Description
shows connect button when no trackers
Empty state with "Connect a Fitness Tracker"
shows tracker list when connected
Renders ConnectedDeviceCard for each tracker
connect button opens picker
Tapping connect opens DevicePickerModal
shows loading state
Shows skeleton while fetching trackers
handles fetch error
Shows error message when fetch fails
useConnectedTrackers.test.js
Test Case
Description
returns loading initially
isLoading=true before fetch completes
returns trackers on success
data contains array of user's trackers
returns empty array for new user
data=[] when user has no trackers
returns error on failure
error populated when fetch fails
filters by current user
Only returns trackers for authenticated user
includes last_synced timestamps
Each tracker has last_synced field
refetches on invalidation
Calling invalidate triggers refetch
Test Case
Description
calls Edge Function with tracker_id
POST to sync-fitbit with correct payload
returns loading during sync
isLoading=true while syncing
updates last_synced on success
Tracker's last_synced updated after sync
invalidates trackers query
useConnectedTrackers refetches on success
returns error on failure
error populated when sync fails
handles token refresh error
Shows re-auth prompt on 401
useDisconnectTracker.test.js
Test Case
Description
deletes tracker from database
Removes row from fitness_trackers
deletes associated tokens
Removes row from fitbit_tokens
invalidates trackers query
List refreshes after disconnect
returns error on failure
error populated when delete fails
9.3 Backend Edge Function Tests
fitbit-auth-start.test.ts
Test Case
Description
generates secure state parameter
State includes user_id + random nonce
stores state in database
State saved to oauth_states table with expiry
returns redirect to Fitbit
302 redirect to api.fitbit.com/oauth2/authorize
includes required scopes
URL has scope=activity+sleep+heartrate+profile
includes correct redirect_uri
Callback URL matches Edge Function URL
requires authentication
Returns 401 if no auth token
Test Case
Description
verifies state parameter
Rejects if state doesn't match stored state
rejects expired state
Returns error if state older than 10 minutes
exchanges code for tokens
POST to api.fitbit.com/oauth2/token succeeds
stores tokens in fitbit_tokens
Tokens upserted with user_id as key
creates fitness_tracker record
New row in fitness_trackers with provider='fitbit'
returns success HTML
Response includes postMessage script
handles invalid code
Returns error page for bad authorization code
handles Fitbit API error
Returns error page if token exchange fails
stores fitbit_user_id
Saves Fitbit's user ID for API calls
triggers initial backfill
Queues sync job for all historical data
Test Case
Description
refreshes expired token
Calls refresh endpoint if expires_at < now
updates stored token after refresh
New access_token saved to fitbit_tokens
fetches daily summary
GET /1/user/-/activities/date/{date}.json
transforms daily summary
Maps Fitbit fields to daily_summary schema
stores daily summary
Upserts into daily_summary table
fetches sleep sessions
GET /1.2/user/-/sleep/date/{date}.json
transforms sleep data
Maps sleep stages to sleep_sessions schema
stores sleep sessions
Upserts into sleep_sessions table
fetches activities
GET /1/user/-/activities/list.json
transforms activities
Maps activity fields to activities schema
stores activities
Upserts into activities table
fetches intraday heart rate
GET /1/user/-/activities/heart/date/{date}/1d/1min.json
transforms intraday data
Maps HR readings to intraday_data schema
stores intraday data
Batch inserts into intraday_data table
uses incremental sync
Only fetches data after last_synced
updates last_synced timestamps
Updates fitness_trackers.last_synced
logs to ingestion_log
Creates success/failure log entry
handles rate limit (429)
Retries with backoff, logs warning
handles auth error (401)
Marks tracker as needing re-auth
handles partial failure
Continues with other data types if one fails
Test Case
Description
validates webhook signature
Rejects requests with invalid signature
handles verification request
Returns 204 with verify parameter
parses notification payload
Extracts collectionType and ownerId
triggers sync for activities
Queues sync when collectionType=activities
triggers sync for sleep
Queues sync when collectionType=sleep
triggers sync for body
Queues sync when collectionType=body
logs webhook receipt
Creates entry in ingestion_log
handles unknown user
Logs warning for unrecognized ownerId
returns 204 on success
Always returns 204 to Fitbit
9.4 Data Transformer Tests
transformFitbitDailySummary.test.ts
Test Case
Description
maps steps correctly
summary.steps → steps
converts distance to meters
summary.distances[total] * 1000 → distance_m
maps calories
summary.caloriesOut → calories_total
maps resting HR
summary.restingHeartRate → resting_heart_rate
preserves vendor_metadata
activityCalories, sedentaryMinutes, etc. in JSON
handles missing optional fields
Returns null for missing fields
sets correct activity_date
Date string in YYYY-MM-DD format
transformFitbitSleep.test.ts
Test Case
Description
maps start_time
sleep.startTime → start_time (ISO 8601)
maps end_time
sleep.endTime → end_time (ISO 8601)
calculates duration
minutesAsleep → total_duration_minutes
preserves sleep stages
deep, light, rem, wake in vendor_metadata
handles multiple sessions
Returns array for multiple sleep periods
handles classic vs stages
Works with both Fitbit sleep data formats
transformFitbitActivity.test.ts
Test Case
Description
maps activity_type
activityName → activity_type
maps start_time
startTime → start_time (ISO 8601)
converts duration
activeDuration (ms) / 1000 → duration_sec
converts distance
distance (km) * 1000 → distance_m
maps calories
calories → calories_burned
preserves raw data
Full activity object in vendor_metadata
transformFitbitIntraday.test.ts
Test Case
Description
maps timestamp
time + date → timestamp (ISO 8601)
maps heart_rate
value → heart_rate
handles step deltas
steps intraday → steps_delta
batch transforms array
Handles 1440 data points efficiently
deduplicates by timestamp
Prevents duplicate entries on re-sync
Test Case
Description
user can select own trackers
SELECT succeeds for own user_id
user cannot select others' trackers
SELECT returns empty for other user_id
user can insert own tracker
INSERT succeeds with own user_id
user cannot insert for other user
INSERT fails with other user_id
user can delete own tracker
DELETE succeeds for own tracker
user cannot delete others' tracker
DELETE fails for other user's tracker
service role bypasses RLS
Service role can access all data
Test Case
Description
user can select via tracker join
SELECT succeeds for own tracker's data
user cannot select others' data
SELECT returns empty for other user's tracker
service role can insert
Edge Function can insert for any tracker
Test Case
Description
tokens not accessible to users
SELECT fails for users (service role only)
service role can read tokens
Edge Functions can read for sync
service role can update tokens
Token refresh updates work
Test Case
Description
complete Fitbit connection
User clicks Connect → OAuth → tracker created
initial backfill runs
Historical data synced after OAuth complete
UI updates after connection
Profile shows new tracker card
Test Case
Description
sync button triggers full sync
All data types fetched and stored
UI shows syncing state
Button disabled, spinner shown
last_synced updates
Tracker card shows "Just now"
Test Case
Description
disconnect removes tracker
fitness_trackers row deleted
disconnect removes tokens
fitbit_tokens row deleted
disconnect removes data (optional)
Associated data optionally purged
UI updates after disconnect
Tracker card removed from list
mobile/
├── __tests__/
│ ├── components/
│ │ └── FitnessTracker/
│ │ ├── ConnectedDeviceCard.test.jsx
│ │ ├── DevicePickerModal.test.jsx
│ │ └── TrackerAuthWebView.test.jsx
│ └── utils/
│ └── fitnessTrackers/
│ ├── useConnectedTrackers.test.js
│ ├── useSyncTracker.test.js
│ └── useDisconnectTracker.test.js
supabase/
├── functions/
│ ├── fitbit-auth-start/
│ │ ├── index.ts
│ │ └── index.test.ts
│ ├── fitbit-callback/
│ │ ├── index.ts
│ │ └── index.test.ts
│ ├── sync-fitbit/
│ │ ├── index.ts
│ │ ├── index.test.ts
│ │ └── transformers/
│ │ ├── dailySummary.ts
│ │ ├── dailySummary.test.ts
│ │ ├── sleep.ts
│ │ ├── sleep.test.ts
│ │ ├── activity.ts
│ │ ├── activity.test.ts
│ │ ├── intraday.ts
│ │ └── intraday.test.ts
│ └── fitbit-webhook/
│ ├── index.ts
│ └── index.test.ts
Database : Create whoop_tokens and oura_tokens tables, add RLS policies
UI : Build Connected Devices components, add to profile screen
OAuth : Create Fitbit auth Edge Functions
Sync : Implement Fitbit data sync Edge Function
Webhooks : Set up Fitbit Subscription API
Cron : Configure pg_cron for daily sync
Expand : Repeat for Whoop, then Oura