OpenFit is an Expo and React Native app for people who want a clearer view of their Google Health data across iOS, Android, and web. The repository name is fitty, but the installed app name in app.json is OpenFit.
The app signs in with Google, reads Google Health API data, and turns it into a configurable health dashboard. It can show activity, sleep, heart, body, and nutrition metrics; keep home-screen widgets in sync; and, on iOS, manually export selected Google Health records into Apple Health.
OpenFit is built around four pieces:
- A Google Health dashboard with date ranges for today, 7 days, 14 days, 30 days, and 90 days.
- Configurable rings, cards, and widget metric slots, so the user can decide which metrics matter on the dashboard and on the home screen.
- Native widgets for iOS and Android that show the latest synced metric values outside the app.
- An iOS-only Apple Health export that writes Google Health weight, sleep, and workout records to HealthKit after the user starts the sync.
The app requests read-only Google scopes. It does not write data back to Google Health. The Apple Health flow is one-way:
Google Health API -> OpenFit normalization and sync ledger -> Apple HealthKit
That export is intentionally limited. It supports weight, sleep, and workouts because those map cleanly to HealthKit records. It does not mirror steps, heart-rate streams, active energy streams, or distance totals yet because those are easier to duplicate or double count without a stronger import history.
- Google sign-in through OAuth, using the app's Expo Router API routes for config, callback, token exchange, and token refresh.
- Google Health reads for activity and fitness, profile, sleep, health metrics and measurements, and nutrition.
- Metric catalog for steps, calories, distance, floors, elevation, sedentary time, swim strokes, VO2, heart rate, HRV, oxygen saturation, respiratory rate, body metrics, nutrition, hydration, and sleep.
- Dashboard customization for ring metrics, goals, and visible metric cards.
- Snapshot caching so the dashboard can show recent data quickly while it refreshes.
- iOS widget support through
expo-widgets. - Android widget support through
react-native-android-widget. - Local Expo module in
modules/apple-health-syncfor HealthKit authorization and writes. - HealthKit sync metadata and a small SecureStore ledger to avoid rewriting unchanged Apple Health records.
- Web support with Expo Router server output and Google OAuth API routes.
src/app Expo Router screens and API routes
src/app/index.tsx Main health dashboard
src/app/settings.tsx Account, widget, and Apple Health sync settings
src/app/api/google OAuth config, callback, token, refresh, and session routes
src/components Dashboard cards, rings, tabs, editors, and shared UI
src/lib Google Health, OAuth, widget, cache, prefs, and sync logic
src/widgets iOS and Android widget renderers and task handlers
modules/apple-health-sync
Local Expo module that bridges to HealthKit on iOS
docs Notes for Google Health -> Apple Health sync behavior
app.json Expo app config, bundle IDs, HealthKit entitlement, widgets
eas.json EAS build profiles
The generated ios/ and android/ folders are not committed. Expo prebuild creates them when you run or build the native app.
- Expo SDK 56
- React 19.2.3
- React Native 0.85.3
- Expo Router
- Bun for dependency installation and scripts
- TypeScript
- Expo API routes for the Google OAuth server pieces
- HealthKit through a local Expo module on iOS
Expo SDK 56 requires a modern toolchain. The versioned Expo docs list Node.js 22.13.x as the minimum for SDK 56, React Native 0.85, React 19.2.3, Android SDK 36, and Xcode 26.4 or newer for iOS SDK builds.
Install these before building from source:
- Git
- Bun
- Node.js 22.13.x or newer
- Xcode 26.4 or newer for iOS builds
- Android Studio with Android SDK 36 for Android builds
- An Expo account and EAS CLI if you want cloud builds
- A Google Cloud project with OAuth credentials that can request the Google Health scopes used by the app
For iOS HealthKit export, use a native iOS build. Expo Go can run parts of the JavaScript app, but it cannot include this repo's native HealthKit module or widget targets.
Clone the repo and install dependencies:
git clone <repo-url>
cd Fitty
bun installCreate .env.local in the project root:
GOOGLE_CLIENT_ID=your-web-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-web-client-secretOptional values for native or deployed builds:
# Use a stable API origin instead of the local Metro origin.
EXPO_PUBLIC_API_BASE_URL=https://your-domain.example
# Force Google to return to a specific OAuth callback.
EXPO_PUBLIC_GOOGLE_REDIRECT_URI=https://your-domain.example/api/google/callback
# Override the native app return URI if you change the scheme.
EXPO_PUBLIC_GOOGLE_APP_RETURN_URI=fitty:/oauth
# Enable extra debug details in the UI.
EXPO_PUBLIC_DEBUG=1When deploying the API routes, set the same OAuth values in the hosting environment. .env.local is only for local development.
Create a Web OAuth client in Google Cloud Console. Add every callback URI the app will use as an authorized redirect URI.
For local simulator development, add:
http://localhost:8081/api/google/callback
For a deployed build, add:
https://your-domain.example/api/google/callback
For native development on a physical device, use a stable callback URI. Google requires exact redirect URI matches, so LAN URLs like http://192.168.x.x:8081/api/google/callback only work if that exact URL is added to the OAuth client. In practice, it is usually cleaner to set EXPO_PUBLIC_API_BASE_URL and EXPO_PUBLIC_GOOGLE_REDIRECT_URI to a deployed or tunneled API origin.
The native app return URI is different from the Google redirect URI. Google redirects to /api/google/callback; that route then sends the user back into the app with fitty:/oauth.
Start the Expo development server:
bun run startRun web:
bun run webRun iOS from source:
bun run iosRun Android from source:
bun run androidbun run ios and bun run android compile native projects locally. If ios/ or android/ do not exist yet, Expo generates them from app.json, the local Expo module, and the configured plugins.
Use a native build for the full app:
- iOS HealthKit export depends on
modules/apple-health-sync. - iOS widgets depend on the
expo-widgetsconfig inapp.json. - Android widgets depend on
react-native-android-widget. - The app's generated native projects include the bundle ID/package
com.francescooddo.fitty.
If native config changes and you want a clean generated project, run:
bunx expo prebuild --cleanThen build again:
bun run ios
# or
bun run androidDo not commit the generated ios/ or android/ folders unless the project intentionally switches away from the current generated-native-folder workflow.
The repo includes eas.json with three profiles:
development: internal distribution with a development clientpreview: internal distributionproduction: store build with remote auto-incremented versions
Install or run EAS CLI with a version that satisfies eas.json:
bunx eas-cli@latest whoamiBuild a preview binary:
bunx eas-cli@latest build --profile preview --platform ios
bunx eas-cli@latest build --profile preview --platform androidBuild production binaries:
bunx eas-cli@latest build --profile production --platform ios
bunx eas-cli@latest build --profile production --platform androidRun the same EAS build process on your machine:
bunx eas-cli@latest build --profile production --platform ios --local
bunx eas-cli@latest build --profile production --platform android --localThe iOS production profile is configured for App Store Connect app ID 6779281959. Release builds need the correct Apple and Google Play signing credentials.
If you use the development EAS profile, make sure the project includes expo-dev-client, because that profile is meant to produce a development-client build.
Run TypeScript:
bunx tsc --noEmitRun lint:
bun run lintExport the iOS JavaScript bundle:
bunx expo export --platform ios --output-dir dist/iosBuild artifacts, dependencies, native generated folders, and local env files are ignored by git.
Missing GOOGLE_CLIENT_ID in .env.local
The API route cannot see the OAuth environment variables. Check .env.local for local development, or the hosting provider's environment settings for deployed API routes.
redirect_uri_mismatch
Add the exact callback URI shown by the app to the Google OAuth Web client. The path should end in /api/google/callback.
Google sign-in works on web but not on a native device
The native app may be using a LAN or production API origin that is different from the web origin. Set EXPO_PUBLIC_API_BASE_URL and EXPO_PUBLIC_GOOGLE_REDIRECT_URI to a stable origin and add that callback to Google Cloud Console.
Apple Health export is unavailable
Apple Health sync only runs on iOS and needs a native build. It will not work as a real HealthKit bridge in Expo Go, on Android, or on web.
Widgets are empty or stale
Open the app, sign in with Google, then sync from the dashboard or settings. Widgets use the latest stored widget data, so they need at least one successful sync before they can show real values.
- Expo SDK 56 docs: https://docs.expo.dev/versions/v56.0.0/
- Expo CLI docs: https://docs.expo.dev/more/expo-cli/
- EAS Build docs: https://docs.expo.dev/build/introduction/
- Google Health to Apple Health implementation notes:
docs/google-health-to-apple-health-sync.md