# Ship Your Expo Fitness App to the App Store with Ease

> simplify your app deployment with a kit that automates the complex process of building and submitting your Expo fitness app to the App Store and Play Store.
> By Dave · 2026-07-18
> Source: https://otf-kit.dev/blog/the-app-store-submission-nobody-warns-you-about

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](https://cdn.otf-kit.dev/blog/the-app-store-submission-nobody-warns-you-about/inline-1.png)



## 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:

```json
{
  "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.

## The day-of commands

A normal release day, end to end:

```bash
# 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:

```bash
#!/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.