Mobile Delivery Basics
Ten examples for coordinating native store releases with OTA JavaScript trains - the operational foundation enterprise mobile teams need before scaling past one engineer tagging Friday builds.
Search across all documentation pages
Ten examples for coordinating native store releases with OTA JavaScript trains - the operational foundation enterprise mobile teams need before scaling past one engineer tagging Friday builds.
Enterprise delivery assumes you already have EAS Build and EAS Update configured:
npx create-expo-app@latest EnterpriseDelivery --template blank-typescript@sdk-57 --yes
cd EnterpriseDelivery
npx expo install expo-updates
npx eas init
npx eas update:configure
npx eas build:configureConfirm the stack pin:
{
"dependencies": {
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0"
}
}Tooling: These examples target Expo SDK 57 (
expo~57.0.4), React Native 0.86.0, and React 19.2.3. Read EAS Build Basics and OTA Updates Basics before coordinating trains.
Every enterprise release decision starts with which clock the change belongs to:
Clock A - Store binary (eas build + eas submit)
Changes: native modules, SDK bumps, permissions, entitlements, icons
Cadence: biweekly or monthly release train
Rollback: new store version (hours to days)
Clock B - OTA bundle (eas update)
Changes: JS, TypeScript, assets, copy - same runtimeVersion as Clock A
Cadence: daily to weekly between store trains
Rollback: eas update:rollback (minutes)| Clock | CI entrypoint | Owner | User impact if mistimed |
|---|---|---|---|
| A | eas build on tag | Mobile platform | Crash on launch, store rejection |
| B | eas update on merge | Mobile feature | Wrong UI, JS errors on old binary |
Use a simple decision table in every PR template:
| Change | Clock | Why |
|---|---|---|
| Button color fix | B (OTA) | Pure JS |
New expo-camera dependency | A (store) | Native module + permission strings |
| API client reads new optional field | B (OTA) | Tolerant reader in JS |
| Hermes / SDK 57 bump | A (store) | Native runtime change |
| Swap hero image asset | B (OTA) | Bundled asset |
Add NSCameraUsageDescription | A (store) | Info.plist in binary |
# PR checklist snippet - paste into .github/pull_request_template.md
## Delivery path
- [ ] JS-only → preview soak → production OTA
- [ ] Native change → eas build required; OTA blocked until new binary shipsRelated: OTA Updates Basics - what OTA can and cannot change
runtimeVersion is the contract between Clock A and Clock B:
// app.config.ts
import type { ExpoConfig } from "expo/config";
const config: ExpoConfig = {
name: "EnterpriseDelivery",
slug: "enterprise-delivery",
version: "2.5.0",
runtimeVersion: {
policy: "appVersion", // "2.5.0" line receives 2.5.x OTAs only
},
updates: {
url: "https://u.expo.dev/<project-id>",
},
ios: { bundleIdentifier: "com.example.enterprisedelivery" },
android: { package: "com.example.enterprisedelivery" },
};
export default config;// eas.json - channel embedded at build time
{
"build": {
"production": {
"channel": "production",
"environment": "production"
}
}
}runtimeVersion: 2.5.0 receive OTAs published for 2.5.0 onlyversion to 2.6.0 without a new store build strands 2.5.0 users on stale JSfingerprint and sdkVersion alternativesEvery production binary must listen on the same channel you publish to:
# Build production binary - channel baked in at compile time
eas build --profile production --platform all --non-interactive
# OTA must target the same channel
eas update --channel production --environment production \
--message "fix: checkout total rounding"# Verify alignment before every production OTA
eas channel:view production
# Confirm channel points at production branch and runtimeVersion matches app.configEnterprise teams run one shared calendar - not separate iOS, Android, and JS spreadsheets:
# Release 2.5.0 - Week of 2026-07-14
## Clock A (store)
- Thu W1: code freeze 18:00 UTC
- Fri W1: tag v2.5.0; eas build production; gated submit
- Mon W2: Play staged 10% → 50%
- Wed W2: ASC phased release 100%
## Clock B (OTA)
- Mon–Wed W1: OTA to preview channel only (behind flags)
- Thu W1: freeze OTA to production until store RC signed
- Wed W2: OTA hotfix lane open if runtimeVersion unchanged
## Dependencies
- [ ] API v3 on prod - @backend - Thu W1
- [ ] Feature flag checkout_v3 at 0% - @mobile - Fri W1runtimeVersion is stableThe artifact QA tested must be the artifact stores receive:
# Release pipeline records build_id in ticket
eas build --profile production --platform ios --non-interactive
# build_id: abc123-def456-...
# QA signs off on abc123 - never rebuild before submit
eas submit --platform ios --id abc123-def456-... --non-interactive# .github/workflows/release.yml - submit pinned ID from workflow output
- name: Submit iOS (pinned build)
run: eas submit --platform ios --id ${{ needs.build.outputs.ios_build_id }} --non-interactive--latest submit is for emergencies only - document when it is allowedJS that calls a new API must not ship OTA until the backend is live:
Wrong order: OTA ships Monday → API deploys Thursday → 3 days of 404s
Right order: API deploys Thursday (backward compatible) → OTA ships Friday// src/api/checkout.ts - tolerant reader until flag ramps
export async function fetchCheckoutConfig(): Promise<CheckoutConfig> {
const res = await api.get("/v3/checkout/config");
if (res.status === 404) {
return api.get("/v2/checkout/config"); // fallback for old path
}
return res.json();
}Store binaries can sit in review for days while JS ships daily - flags decouple live from visible:
Day 1: eas build ships v2.5.0 to TestFlight - checkout_v3 flag OFF
Day 3: eas update ships checkout UI - still hidden behind flag
Day 5: App Review approves; staged rollout begins - flag 0%
Day 7: OTA + flag ramp to 10% internal cohort
Day 10: flag 100% after metrics passWhen the train is JS-only, skip Clock A but keep discipline:
# Mon - merge behind flags; publish preview
eas update --channel preview --environment preview --message "feat: loyalty points UI"
# Tue - QA soak on preview binary (same runtimeVersion as production)
# Wed - staged production rollout
eas update --channel production --environment production \
--rollout-percentage=10 --message "rollout: loyalty @ 10%"
# Thu - monitor Sentry; roll back if crash-free drops
# Fri - 100% if metrics holdDocument rollback before you need it - enterprise incidents move faster with pre-written cards:
| Clock | Trigger | Action | ETA |
|---|---|---|---|
| B (OTA) | Crash-free -2% after publish | eas update:rollback + flag off | < 15 min |
| B (OTA) | Payment success -1% | Flag off first; rollback second | < 10 min |
| A (store) | Native crash on launch | Halt staged rollout; promote previous version | 1–24 hr |
| A (store) | Store rejection | Fix binary; resubmit - OTA cannot fix native | days |
# OTA rollback card - fill per release
eas update:list --branch production --limit 3
# Last known-good: <update-group-id>
eas update:rollbackYes, if runtimeVersion matches the binary in review and the OTA does not change behavior reviewers will test. Keep production OTAs behind flags until approval - reviewers may test a build that receives an OTA on launch.
Biweekly is common for consumer apps; monthly for regulated industries. OTA fills the gap between trains - daily or weekly for JS fixes. Match cadence to store-review risk and backend dependency windows.
Engineering manager or release captain - accountable. Mobile, backend, QA, and support are consulted. The calendar lives in a shared doc or Jira epic, not one engineer's notebook.
One eas update serves both platforms when they share runtimeVersion and channel. Store submits remain per-platform - EAS Submit.
Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026