Skip to content
OTFotf
All posts

Ship Your Expo Fitness App to the App Store with Ease

D
DaveAuthor
6 min read
Ship Your Expo Fitness App to the App Store with Ease

Local development runs your app. The App Store does not.

The gap between "I can see it on my phone" and "a real user can download it from a store" is six distinct systems, two vendor accounts, three signing identities, and a review queue you cannot bypass. If you learned native development by iterating in the dev client, the production path looks like a completely separate product — because for the bundle, the credentials, and the binary pipeline, it is.

The Fitness kit ships that second product pre-wired. You do not learn it the hard way. Here is what the wiring looks like, what the scripts do, and where they save you a week.

The three pipelines you actually need

Your app icon has three real journeys before it exists on a stranger's home screen:

  1. iOS — Apple Developer Program enrollment ($99/yr), an App Store Connect app record, a distribution certificate, a provisioning profile, an App Store build, TestFlight upload, App Review.
  2. Android — Play Console account ($25 one-time), app listing, upload key + keystore, AAB upload, internal/closed/open testing tracks, Play review (faster turnaround, stricter policy enforcement).
  3. The orchestrator — a service that ties the two together with sane defaults: EAS Build + EAS Submit. Runs your prebuild, signs with the right identity, uploads to both stores from one command.

You can do all of this by hand. People do. The hand-rolled path is xcodebuild for iOS, gradle for Android, two separate credential flows, and a fastlane/ directory you will rewrite every six months when Xcode's signing model changes.

dev loop vs store-ready build

What the config actually looks like

Every native build needs a config at the root. Here is what the kit ships — three build profiles, two submit profiles, default-track aware:

{
  "cli": {
    "version": ">= 12.0.0",
    "appVersionSource": "remote"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal",
      "ios": { "distributionType": "store", "credentialsSource": "remote" }
    },
    "production": {
      "ios": { "distributionType": "app-store" },
      "android": { "buildType": "app-bundle" }
    }
  },
  "submit": {
    "production": {
      "ios": { "ascAppIdentifier": "REPLACE_ME", "appleTeamId": "REPLACE_ME" },
      "android": { "track": "internal" }
    }
  }
}

The REPLACE_ME placeholders are filled once during pnpm ship:init with a wizard that walks through Apple Developer, App Store Connect, and Play Console. They never land in a real commit.

One codebase. iOS, Android, and web.

The Fitness Kit ships with auth, a database, and a backend already connected — no setup. Live demo at fitness-preview.otf-kit.dev.

See the live demo

The day-of commands

A normal release day, end to end:

# Bump the version (remote, so both platforms share it)
eas build:version:set --platform all --message "v1.0.3"

# Build for internal QA — signed IPA + AAB in the cloud, no Xcode locally
eas build --profile preview --platform all

# Submit to TestFlight (iOS) and the internal testing track (Android)
eas submit --platform all --latest

Three commands. The kit ships an eas-submit.sh wrapper that adds the version bump, the --non-interactive flag for CI, and a Slack-notify hook so the channel learns about the upload without polling a dashboard.

Where the scripts earn their keep

The reason scripts ship with the kit is not that the commands are long. It is because each of these lands a first-time shipper in App Review purgatory:

  • Privacy manifest. Current iOS requires PrivacyInfo.xcprivacy declaring every API you touch. Without it, Apple rejects. The kit ships one pre-filled for an auth-heavy fitness app.
  • Usage descriptions. NSMotionUsageDescription, NSCameraUsageDescription, NSHealthShareUsageDescription. Each native module that touches a privacy-sensitive API needs the string in Info.plist. Missing one is an automatic rejection. The kit writes them at prebuild time.
  • Background modes for workout sessions. UIBackgroundModes must include processing for workout tracking, optionally audio for voice coaching. Wrong combination = rejected on first submission.
  • App Tracking Transparency. If you ship a fitness app with analytics, you cannot fire the IDFA prompt lazily on first launch. Apple expects it before any third-party network call fires.
  • Play Console Data Safety form. Two pages of questions derived from your manifest declarations. Get one answer wrong and your update is paused from search results, not just from download. The kit ships a data-safety.json describing exactly what the analytics modules collect.
  • AAB signing config. A debug-signed AAB installs on your phone and then fails at upload time. The kit pre-binds a release keystore under EAS Credentials, surfaced through a single keystore bootstrap command.

These are not edge cases. Each is a 7-day review cycle you avoid by copying the kit's config and the five files it pre-writes.

What the kit does at prebuild time

The kit's pnpm prebuild is not the bare upstream prebuild. It runs in this order:

#!/usr/bin/env bash
set -euo pipefail

# 1. Sync native folders without wiping custom config
npx expo prebuild --no-install --clean

# 2. Patch Info.plist with the privacy manifest + usage descriptions
node scripts/patch-ios-info-plist.js

# 3. Patch AndroidManifest with the permissions a fitness app needs
node scripts/patch-android-manifest.js

# 4. Write the AAB signing config for release builds
node scripts/patch-android-gradle.js

# 5. Run the design-quality gate before anything ships
node scripts/design-check.js --strict

Five hooks, one command. Each patch script is idempotent — re-running prebuild never duplicates entries, never clobbers user-added keys. They live in scripts/ and they are the reason the kit is deployable out of the box instead of "ready in a weekend".

The AI tooling knows all this

Every kit ships CLAUDE.md, .cursorrules, and ai/prompts/release.md so an agent extending the kit does not have to rediscover the deploy story. The release prompt tells the agent:

Bump versions with eas build:version:set. Never edit Info.plist privacy strings by hand — patch scripts own them. To cut a TestFlight build, run pnpm ship:preview. To cut a production release, run pnpm ship:prod.

That block is what stops a coding agent from "fixing" a build by hand-editing the manifest and breaking the next prebuild. The agent reads the rule and defers to the script — every time.

What this enables

  • Day-one TestFlight beta. pnpm ship:preview from a clean clone. One command, five minutes to a signed IPA, ready to invite testers.
  • OTA updates without redeploying the binary. The kit's production channel is wired to EAS Update, so a JS-only fix lands in users' hands in 60–90 seconds without a review cycle.
  • One release script for both stores. pnpm ship:prod --message "v1.0.3" triggers iOS production build + submit and Android AAB build + Play internal track in parallel, then posts the link back to your channel.
  • A 24-item design gate that runs before submission. Catches a screen that violates contrast or hit-area rules before it ever reaches a human reviewer, where layout rejections are common.

The durable layer

Models and tooling churn. The native pipeline moves. Apple's privacy manifest requirements will change again. What does not change is the principle: your app's native config should be a checked-in artifact, edited by scripts, never by a developer at 11pm debugging a rejection email.

The Fitness kit is that artifact, frozen into scripts/. When a flag is deprecated, the patch script is updated in one place and every app built on the kit inherits the fix. When the coding agent rewrites a screen, the privacy strings and the signing config stay untouched. The free SDK on npm is a starting point; the deploy wiring is what gets you from pnpm start to an icon on someone's home screen.

The free components ship under MIT for any native project. The full Fitness kit (one codebase, iOS + Android + web) bundles every script in this post with the AI-tool configs wired up. Demo: fitness-preview.otf-kit.dev.

react-nativebackendkits
OTF Fitness Kit

Stop wiring. Start shipping.

  • Login, database, and backend already connected — nothing to set up
  • iOS + Android + web from one codebase
  • AI configs pre-tuned + 40+ tested prompts included