The trainee app is built with EAS Build and submitted with EAS Submit. Nothing in CI builds or publishes the app. A developer runs the commands from mobile/ with the EAS CLI, signed in to the Expo account that owns the project. eas.json requires EAS CLI version 20 or newer.

App identity

From mobile/app.json: The iOS app includes a widget extension target, ExpoWidgetsTarget, declared under extra.eas.build.experimental.ios.appExtensions with an app group entitlement. The expo-widgets plugin config declares three home screen widgets: WorkoutToday, NutritionToday and Streak.

Build profiles

eas.json defines three profiles.
The preview profile points at the production API. A preview build reads and writes real data. There is no separate staging API in the configuration.
cli.appVersionSource is remote. EAS stores the build number (iOS buildNumber, Android versionCode) on its servers and increments it for each production build. The user-facing version comes from expo.version in app.json. Keep version in package.json equal to it.

Commands

All scripts are in mobile/package.json.

Build

The local iOS scripts put the system directories first on PATH so the build uses the system toolchain and not a version manager’s shims.

Build and submit

The submit.production profile sends iOS builds to App Store Connect and Android builds to the production track with release status completed.
pnpm playstore and pnpm release submit the Android build straight to the production track as a completed release. There is no internal or staged track in the profile. Be sure before you run them.
Signing credentials and store API keys are managed by EAS and are not in the repository. Ask the project owner which submission key to use.

Run locally

Native configuration

ios/ and android/ are generated by prebuild. Change native settings in these places instead of editing the generated folders:
  • app.json, including the plugin list: expo-build-properties, expo-router, expo-splash-screen, expo-secure-store, expo-image, expo-image-picker, expo-notifications, expo-local-authentication, expo-localization, expo-sharing, expo-camera, expo-media-library, expo-widgets, expo-video, @kingstinct/react-native-healthkit, react-native-health-connect, expo-task-manager, expo-background-task, expo-navigation-bar.
  • The two local config plugins in mobile/plugins, which add the Health Connect permission delegate and the permissions rationale activity on Android.
  • The local native module in mobile/modules/cardio-notification, an Android ongoing notification with a native chronometer for cardio sessions.
  • The patches in mobile/patches, applied by pnpm at install time.
Permissions are declared in app.json. Android requests exact alarms, biometrics, camera, audio recording, and Health Connect step reading including background reads. It explicitly blocks the broad media read permissions. iOS declares HealthKit with background delivery and time-sensitive notifications.

When a new store build is required

An OTA update can only change JavaScript and assets. Cut a new store build for both platforms when you:
  • add, remove or upgrade a dependency that contains native code,
  • add or change a config plugin or its options,
  • change permissions, entitlements, infoPlist values, icons or the splash screen,
  • change the local native module or a patch that touches native code,
  • upgrade Expo or React Native,
  • bump expo.version.
Everything else can go out as an OTA update. See OTA updates for the rule that connects the two.

Bumping the version

  1. Change version in app.json and in package.json to the same value.
  2. Build and submit both platforms.
  3. After the builds are live in the stores, set TRAINEE_APP_IOS_VERSION and TRAINEE_APP_ANDROID_VERSION on the backend to the new version. The app compares its own version with these and prompts the trainee to update when it is older. Leave them unchanged until the store listing actually serves the new build, or trainees are sent to a store page that has nothing newer.
  4. Add the previous version to the list of runtimes that still need OTA updates. See OTA updates.
Because the runtime version policy is appVersion, a new version is also a new OTA runtime. Updates published for the old version do not reach the new binary and the other way round.

Environment variables in builds

EXPO_PUBLIC_* values are compiled into the JavaScript bundle. For EAS builds they come from the env block of the build profile in eas.json. For a local production build they come from mobile/.env.production. Keep the two in sync. The .env.example file says the same. These values are public. Anyone can read them from the app bundle.

The trainee preview export

Coaches see a live preview of the trainee app inside the web app. It is the mobile app exported for the web.
scripts/export-preview.sh runs expo export -p web into a temporary folder with PREVIEW_BASE_PATH (default /trainee-preview) and an optional PREVIEW_API_URL, checks that index.html was produced, then replaces ../frontend/apps/saas/public/trainee-preview with the result. app.config.js reads the two variables and sets experiments.baseUrl and extra.apiUrl for that export only. The web app serves the bundle through a rewrite in apps/saas/next.config.ts. The backend issues short-lived preview tokens for it (PREVIEW_TTL_SECONDS is 15 minutes in trainee.token.ts), and the trainee lane answers FORBIDDEN with reason PREVIEW_READ_ONLY to any non-GET request made with a preview token. Native-only modules do not exist on the web. metro.config.js maps them to the replacements in src/lib/web-shims for the web export.

Checks before a build

Then follow the Release checklist.