Expo & EAS: shipping React Native
SkillDev toolsUse when shipping a React Native app with Expo — EAS Build/Submit/Update, eas.json profiles and channels, config plugins, prebuild/CNG, runtime-version policy, OTA updates that never land, SDK upgrades, the New Architecture. NOT RN UI, navigation or native-module authoring (that is `react-native`), NOT a Dart app (that is `flutter`).
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Expo & EAS: shipping React Native skill
What this skill tells your AI
The instructions your AI receives, as published by ericrisco/rsc-harness in skills/expo/SKILL.md and read by ahel’s review.
Hand-off — the app vs its motion. This skill owns the Expo platform: router, config plugins, EAS,
native modules, store builds. Animation and gestures — Reanimated worklets, the UI runtime,
sheets, screen transitions, press feedback, haptics, motion that stutters on a real device — are
../motion-craft/SKILL.md's.
What this skill owns
Expo is the toolchain and EAS cloud platform layered on React Native: cloud builds, store submission, over-the-air JS updates, and native configuration declared in JavaScript instead of hand-edited Xcode/Gradle projects. This skill owns the shipping pipeline and native-config-via-JS.
The verb rule of thumb: if the verb is build / submit / update / prebuild /
plugin / EAS / channel / runtime-version, you are in expo. If it is render /
navigate / animate / bridge / write a native module, route to react-native.
Other exits: web React/hooks/state → react, a Dart app → flutter, native-only
Swift/Kotlin → swift-ios/kotlin-android, a desktop wrapper → tauri/electron,
CI unrelated to EAS → github-actions.
Current stable as of 2026-06-02: Expo SDK 55 — React Native 0.83.1, React 19.2.0, shipped 2026-02-25. SDK 56 is in beta (beta opened 2026-05-06, ~2-week window; RN 0.85.2, React 19.2.3) — upcoming, not yet shipped stable. Both run exclusively on the New Architecture; the Legacy Architecture was removed in SDK 55.
Decision rules
| Situation | Do this | Why / not that |
|---|---|---|
| New app, want config-as-JS | managed + prebuild (CNG) — no committed ios//android/ | native dirs are regenerable artifacts; hand-edits get blown away |
| You truly need to hand-edit native code long-term | bare (commit ios//android/) | last resort; you lose prebuild --clean upgrades |
| Quick demo, only Expo-SDK modules | Expo Go | zero build, but custom native deps will crash |
| Any custom native dependency or plugin | dev build (developmentClient: true) | Expo Go cannot load arbitrary native code |
| Runtime version, picking a policy | fingerprint (auto-bumps on native change) | safest default; prevents serving JS to an incompatible binary |
| Runtime tied to your release version | appVersion | simpler, but you must remember to bump it on native changes |
The shipping pipeline (core)
Four EAS verbs, in this order: prebuild → build → submit → update.
npx expo prebuild --clean # regenerate native dirs from app config + plugins (CNG)
eas build -p ios --profile production # cloud-build the binary (.ipa/.aab)
eas submit -p ios --profile production # upload to App Store / Play Store
eas update --branch production --message "fix typo" # ship JS-only over the air
prebuild is only needed for managed/CNG apps and runs automatically inside
eas build; run it locally to inspect or to verify a plugin. Build/submit ship a
new binary; update ships JS only — anything touching native code needs a new build.
Minimal eas.json with the three default profiles plus channels. Each build is
stamped with a channel; a channel maps to a same-named EAS Update branch by default
(SDK 55 stable builders: RN 0.83.1, Xcode 26 on iOS, Android 16 target):
{
"cli": { "version": ">= 16.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"channel": "development"
},
"preview": {
"distribution": "internal",
"channel": "preview"
},
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}
Run scripts/verify.sh inside an Expo project to gate eas.json, the runtime
policy, committed secrets, and New-Arch readiness.
EAS Update mental model
Runtime version is the compatibility gate. An update applies to a build only
when BOTH the platform AND the runtimeVersion match exactly — there is no
"close enough". The chain is: build stamped with a channel → channel points at a
branch → you publish updates to a branch. Updates flow only down a matching
runtime within the linked branch.
The #1 footgun: a hardcoded runtimeVersion that drifts from the binary, so the
update silently never installs and you stare at unchanged devices.
// BAD — hardcoded string in app.json; bump a native dep and every old build
// silently stops matching, so your "shipped" OTA never reaches users.
{ "expo": { "runtimeVersion": "1.0.0" } }
// GOOD — fingerprint policy: EAS hashes the native runtime and auto-bumps the
// runtime version whenever native code/config changes, so updates only ever
// reach binaries that can actually run them.
{ "expo": { "runtimeVersion": { "policy": "fingerprint" } } }
Inspect, branch ops, rollouts/rollbacks, republish, and the full "update not
applying" decision flow → references/eas-update.md.
Config plugins / CNG
Never hand-edit ios/ or android/ — they are ephemeral, regenerated by
npx expo prebuild --clean from app.config + plugins + autolinking. To change
native config, write a config plugin (a function in the plugins array) or use a
mod like withInfoPlist / withAndroidManifest.
// app.plugin.ts — add an iOS Info.plist key during prebuild, the CNG way.
import { ConfigPlugin, withInfoPlist } from "expo/config-plugins";
const withCameraUsage: ConfigPlugin<{ reason: string }> = (config, { reason }) =>
withInfoPlist(config, (cfg) => {
cfg.modResults.NSCameraUsageDescription = reason;
return cfg;
});
export default withCameraUsage;
// app.config.ts — dynamic config; reference the plugin with its options.
export default {
expo: {
name: "MyApp",
runtimeVersion: { policy: "fingerprint" },
plugins: [["./app.plugin.ts", { reason: "Scan receipts" }]],
},
};
Plugin anatomy, dangerous mods, mod ordering, and prebuild troubleshooting →
references/config-plugins.md.
New Architecture & SDK upgrade
The Legacy Architecture is gone. SDK 54 was the last release to ship it; SDK 55
(2026-02-25, current stable) removed it entirely, and the SDK 56 beta builds on that.
On any supported SDK you are already on the New Architecture — it is always
enabled and cannot be turned off. The newArchEnabled flag was deleted from
app.json in SDK 55; if you still carry one, it is dead config — remove it. There is no legacy
fallback to lean on, so a dependency that only works on the old architecture is now a
hard blocker, not a "flip the flag back" escape hatch.
Upgrade checklist:
npx expo install expo@latest --fix— bump SDK and align every dependency.npx expo-doctor@latest— catch deps that never made the New-Arch jump before you build.npx expo prebuild --clean— regenerate native dirs (managed/CNG apps).- Build a
development/previewbinary on a device before promoting to production.
Two upgrade tripwires: every Android app is edge-to-edge (on since SDK 54, non-negotiable) — audit manual inset/status-bar code. And SDK 55+ ships Hermes bytecode diffing for EAS Update (~75% smaller OTA downloads) automatically; you get it for free once both the build and the update are on SDK 55+.
Credentials & secrets
- Let EAS manage credentials (signing keys, provisioning profiles) by default — it stores and rotates them server-side so they never touch the repo.
- Supply per-profile config through EAS environment variables /
.envfiles scoped by profile, not committed plaintext. - Never commit a keystore (
*.jks/*.keystore),*.p12, or*.mobileprovision, and never put API keys inapp.config/app.json— anything in app config ships inside the public bundle. See../secure-coding/SKILL.md.
EAS Workflows
EAS Workflows are Expo's own CI: YAML in .eas/workflows/. Jobs use pre-packaged
types (build, submit, update) and chain via needs: + outputs. Route to the
github-actions skill only when the user explicitly wants GH Actions or
non-Expo CI.
# .eas/workflows/release-android.yml — build then submit, chained by needs.
name: Release Android
on:
push:
branches: [main]
jobs:
build:
type: build
params:
platform: android
profile: production
submit:
needs: [build]
type: submit
params:
platform: android
build_id: ${{ needs.build.outputs.build_id }}
Plan limits (set expectations)
EAS Free: 15 Android + 15 iOS builds/month on the low-priority queue only (peak waits can exceed an hour). High-priority queue needs a paid plan; the Production plan includes 2 build concurrencies, with extra concurrency at $50/concurrency/month, up to 5 extra. If a user complains about build queue waits, the fix is usually the plan, not the config. (Pricing per expo.dev/pricing, verified 2026-06-02; re-check before quoting — Expo adjusts tiers and dollar figures.)
Anti-patterns
| Anti-pattern | Do instead |
|---|---|
Editing ios/Info.plist directly | prebuild --clean overwrites it; write a config plugin / withInfoPlist. |
Hardcoding runtimeVersion: '1.0.0' because it is simpler | it drifts from the binary; updates silently stop matching. Use the fingerprint policy. |
| Expecting an OTA update to deliver a bumped native dep | EAS Update is JS-only; native changes need a new eas build. |
| Telling users to "just refresh" when a published update does not land | check the channel→branch and exact runtime match first — wrong channel = no delivery. |
| Testing a custom native module in Expo Go | Expo Go can't load arbitrary native code; build a dev client. |
| Upgrading the SDK and building straight to production | run expo-doctor + a preview build first; there is no Legacy-Arch fallback to catch a New-Arch-incompatible dep. |
Setting newArchEnabled: false to dodge a broken native dep | the flag was removed in SDK 55 and the Legacy Architecture is gone; fix or replace the dep. |
| Committing the keystore so CI can sign | never; let EAS manage credentials or use EAS secrets. |
Putting the API key in app.config extra | app config ships in the public bundle; use EAS env vars / a backend. |
Reaching for GitHub Actions to call eas build | EAS Workflows is the native CI; only reach for github-actions if explicitly required. |
Project grounding (02-DOCS + CLAUDE.md)
In a project with a 02-DOCS/ layer (the harness wiki),
read 02-DOCS/wiki/stack/expo.md first and record this app's shipping decisions
there — managed-vs-bare, runtime-version policy, channel/branch map, SDK/New-Arch
status — linked from the root CLAUDE.md ## Knowledge map, bumping its Updated
date when a convention changes. No 02-DOCS/? Skip silently. Conventions are
recorded, not gated — never block the task on this.
Signals
- GitHub stars
- 82
- Forks
- 3
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
expo- Source
- github.com/ericrisco/rsc-harness