The whole delivery workflow
Start with the change you made. The runtime check determines whether you can publish an update or need a new store binary.
Fast update path
Publish to the intended channel. Only builds with matching platform and runtime receive it.
New binary path
Create signed iOS and Android binaries, submit them, then release through the stores.
Set up once
Examples assume a standard Expo project with generated native projects. Merge these fields into your configuration; preserve your project IDs, identifiers, and existing settings.
eas.json
{
"cli": {
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"environment": "development"
},
"preview": {
"distribution": "internal",
"channel": "preview",
"environment": "preview"
},
"production": {
"channel": "production",
"environment": "production",
"autoIncrement": true
}
}
}A development build includes developer tools. A preview build runs independently for team testing. A production build is intended for stores. For iOS Simulator testing, create a separate profile with ios.simulator: true. Build profiles ↗
Keep four ideas separate
| Concept | What it controls |
|---|---|
| Build profile | How to create the binary.--profile preview |
| Update channel | Which update stream the installed app follows.--channel preview |
| Environment | Configuration values used during the job.--environment preview |
| Runtime version | Compatibility between the native binary and the update. |
Identical names are a convention; these settings have different jobs. Environments ↗ · Channels ↗
app.json — merge under expo
{
"expo": {
"runtimeVersion": { "policy": "appVersion" }
}
}Keep the update URL and EAS project ID written by eas update:configure. Configure this before making builds. Switching an existing app to a new runtime policy requires a matching build. Runtime policies ↗
Before testing an OTA update
Run eas build --profile preview --platform all, install the preview binaries, and verify runtime compatibility. Preview and production builds intended to receive the same update must use the same app version and compatible native runtime.
Which path does my change take?
| Example | Delivery route | Why |
|---|---|---|
| Text, colors, or JavaScript logic | EAS Update, if compatible | Fits the installed native runtime. |
| A new bundled illustration | EAS Update, if compatible | An ordinary asset can travel with the update. |
| New native module, SDK upgrade, native permissions | Build → Submit → store release | The installed binary must change. |
| Native launcher icon or native splash configuration | Build → Submit → store release | These are packaged into the binary. |
| Backend-only logic | Deploy the backend | No mobile release is needed unless the client also changes. |
Illustrative decisions based on native compatibility. An OTA update still needs to respect the relevant store policies.
Make the release repeatable
EAS Workflows runs jobs defined in .eas/workflows/*.yml. The same services used in the walkthrough can be connected into a repeatable release process.
Start a workflow
Run it manually, or configure repository events such as a push to main.
Check native fingerprints
Look for an existing production build for each platform’s native fingerprint.
Choose a route
No matching build: build and submit.
Matching build: publish an update.
Generate Expo’s production workflow
eas workflow:create --template deploy # Review the generated jobs, environments, and triggers first. eas workflow:run .eas/workflows/deploy.yml
The generated workflow can publish to production or submit builds when run. Set up the store credentials, inspect its conditions, and add your required tests before adopting it. Finding an existing build does not prove it has been released to users.
Workflow setup ↗ · Expo’s full deployment example ↗
Start smaller: a manual Android preview build
Save this example as .eas/workflows/team-preview.yml. It assumes the project and preview profile are configured.
name: Team preview
jobs:
make_preview:
type: build
params:
platform: android
profile: previeweas workflow:run .eas/workflows/team-preview.yml
Output: an installable preview build for team review. Workflow syntax ↗
What if something fails?
Build failure: read the failed job’s logs, fix the issue, and rebuild. Submission failure: check credentials and store requirements. An update not appearing: compare its channel, platform, and runtime with the installed build, then check download and launch behavior. A defective OTA release: stop a partial rollout or use Expo’s rollback tooling for that runtime.
Update recovery ↗What are you trying to do?
Choose a goal to see the recommended route, what you need first, and where to continue.
A short learning path
Know what is installed
Explore the difference between Expo Go, a development build, and the JavaScript it loads.
Development builds + video ↗Share with a tester
Build a preview, install it on a second device, and collect feedback.
Guided tutorial + video ↗Practice the release loop
Follow the first release and JavaScript-fix walkthroughs. Then learn staged rollout and recovery.
Suggested teaching sequence for this guide. Use the linked official tutorials for their screenshots and videos.
Choose how people test your app
Build type, distribution method, and update channel are separate choices. This table shows common combinations.
| Option | Best suited to | What the tester needs | Main limit |
|---|---|---|---|
| Expo Go | Learning and quick experiments | Expo Go and access to the development server | A fixed native runtime; it does not validate your own release binary. |
| Development build | Developers debugging an app | Your development client; a server for live coding, or a compatible published update | Rebuild when required native capabilities change. |
| Internal preview | A small team reviewing a release-style app | An install link; on physical iOS devices, ad hoc provisioning includes their device | Device provisioning and platform distribution rules apply. |
| TestFlight / Play testing | Testing store-distributed binaries | Access to the relevant store test group or track | Store setup and processing; review requirements depend on the testing route. |
| Public stores | General users | A released app-store version | Listing, compliance, review, and release requirements. |
Expo Go vs development builds ↗ · Review options ↗ · Internal distribution ↗
Walkthrough: a nontechnical tester
- Developer: build with the preview profile from Setup.
- Developer: share the build’s installation link with the tester.
- Tester: install the binary and launch it without a local development server.
- Tester: create a journal entry, restart, and confirm the entry remains.
- Developer: publish a compatible preview update; the tester downloads it and relaunches to verify the change.
eas build --profile preview --platform android
For physical iOS devices, register the device before creating the ad hoc build, or re-sign a suitable existing build. Device setup ↗
Keep test and public apps separate
To install both on one device, configure app variants with distinct Android package names and iOS bundle identifiers. Give them visibly different names or icons so testers know which one they opened.
Illustrative variants
Trail Notes Preview → preview API and preview channel
Trail Notes → production API and production channel
Separate channels do not create separate installable apps by themselves. Variant configuration may also change the native fingerprint; verify compatibility for each variant.
Configure app variants ↗Release gradually, then verify
Example: introduce a new journal filter to a small portion of eligible installations. The percentages below are an example plan, not Expo defaults.
Verify the candidate
Test the filter with an empty journal, existing entries, and offline use. Confirm production configuration.
Start at 10%
Publish a per-update rollout. Compare errors and core task completion with the previous version.
Expand or revert
If healthy, increase the rollout percentage. If the candidate causes failures, revert the rollout.
# Publish a compatible update to a subset of eligible users eas update --channel production --environment production --message "New journal filter" --rollout-percentage 10 # Select the update and increase its rollout percentage eas update:edit # Or select the rollout to revert eas update:revert-update-rollout
Per-update rollout example; assumes no active branch-based rollout on this channel. These commands change a release when you run them. Rollout guide ↗
Recover after an update has been fully released
Run eas update:rollback and follow the prompts to select a prior published update or the update embedded in the build. Confirm the affected runtime and the recovery behavior on a device. An OTA rollback does not remove native code or reverse backend data changes.
“My update is not showing”
Work through the checks in order. Checkmarks are temporary and reset when you reload this guide.
0 of 7 checks completed
Update debugging guide ↗“The build or release failed”
The build failed
Open the failed build’s logs and locate the first actionable failure. Identify whether it happened during dependency installation, app configuration, signing, JavaScript bundling, or native compilation. Reproduce the relevant step locally where possible.
Build troubleshooting ↗The upload failed
Open the submission logs. Check the selected binary, app identifiers, store credentials, and whether the store account and app record are ready. Use the error message to choose the next fix.
Submission process ↗The upload succeeded but users cannot see it
Check store processing, test-track access, listing completion, review status, release selection, and regional availability in the store console.
Store release steps ↗The new app crashes on launch
Collect device logs and compare the affected build and update IDs. Test a release-style build. Determine whether the failure is in the update, native binary, or backend before choosing a rollback or a new build.
Crash investigation ↗Channel versus branch, in plain language
Channel = the address saved in the app
A production build normally asks for the production channel every time it checks for an update. That channel is chosen when the binary is built.
Think “audience.”
production = customers
preview or staging = testers
Branch = the release history behind that address
A branch is an ordered stream of published updates. A channel points to a branch on Expo’s servers, and the pointer can change.
Think “playlist.”
The channel is the station preset.
The branch is the playlist currently playing.
See the relationship
Follow either row from the installed app to the compatible update. Use the three chapters inside the interactive diagram to see production, testing, and promotion.
production channelpreview channelproduction at candidate-v1. Customer binaries still ask for production; no rebuild is required when the update is compatible.Three real-world examples
| Situation | What happens | Concrete example |
|---|---|---|
| Small team: keep it simple | Keep each channel linked to a branch with the same name. You rarely need to manage branches directly. | eas update --channel production --environment production --message "Fix checkout copy"The production build asks for production; production points to the production branch. |
| Test before customers receive it | Publish the same commit for staging or preview first, verify it on the corresponding build, then publish or republish it to production. | Staging build → staging channel → staging branch → tested update. Then use eas update:republish --destination-channel production when your signing and environment setup allows the exact tested bundle to be promoted. |
| Advanced branch promotion | Both staging and production channels can point to the same versioned branch after QA. Changing this pointer happens on the server. | Testers: staging → version-2.0. After approval: eas channel:edit production-rtv-1 --branch version-2.0.Customer binaries keep their embedded channel; that channel now resolves to the tested branch. |
Where platform fits
Platform does not replace the channel or branch. After the channel resolves to a branch, EAS chooses the newest update whose platform and runtime version exactly match the installed build.
Example
An iPhone on runtime 1.0 ignores an Android update and an iOS update for runtime 2.0, even when both are on the selected branch.
Which model should you use?
Most apps: use production and staging channels, let their branches have the same names, and think mainly about channels and runtime versions.
Advanced release teams: manage versioned branches when you need pointer-based promotion, long-lived release histories, or branch-based rollouts.
Expo’s recommended simple release process ↗ · How update selection works ↗ · Manage branches and channels ↗ · Advanced deployment patterns ↗
Set up CI/CD without guessing
Follow these steps once for each Expo React Native project. Run workflows manually first. Connect automatic production deployment only after the manual run succeeds.
main → EAS checks the native fingerprint → build and submit when native code changed, or publish an OTA update when a compatible build already exists.1 Confirm the project and accounts
You need an Expo account and an Expo-based React Native project. Store delivery also requires Apple Developer and Google Play Console accounts.
cd your-react-native-project npm install --global eas-cli eas login eas whoami
2 Link the project and configure EAS
Generate EAS build configuration and enable OTA updates. Commit the generated project configuration.
eas build:configure npx expo install expo-updates eas update:configure git add app.json app.config.* eas.json package.json git commit -m "Configure EAS Build and Update"
3 Define development, preview, and production
Use separate environments and channels. Compare this with your generated eas.json rather than replacing project-specific values blindly.
{
"cli": { "appVersionSource": "remote" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"environment": "development"
},
"preview": {
"distribution": "internal",
"channel": "preview",
"environment": "preview"
},
"production": {
"channel": "production",
"environment": "production",
"autoIncrement": true
}
}
}4 Move cloud configuration into EAS environments
Create the same variable names in preview and production with the appropriate values. Values embedded in client code, including EXPO_PUBLIC_*, are readable by app users and must not contain secrets.
eas env:set --name EXPO_PUBLIC_API_URL --value https://preview-api.example.com --environment preview --visibility plaintext eas env:set --name EXPO_PUBLIC_API_URL --value https://api.example.com --environment production --visibility plaintext eas env:list --environment preview eas env:list --environment production
5 Generate and run the development workflow
Expo’s build template creates .eas/workflows/build.yml and walks through required setup. Run it manually before adding GitHub automation.
eas workflow:create --template build eas workflow:run .eas/workflows/build.yml
Install a successful build from the EAS website, then start Metro with npx expo start.
6 Prepare credentials and prove store builds
Configure signing interactively. Before automating submission, create one successful production build for each platform you intend to ship.
eas credentials:configure-build -p android -e production eas credentials:configure-build -p ios -e production eas build --profile production --platform all
Google Play and App Store Connect require their own app records, agreements, testing, and submission credentials before unattended submission can succeed.
7 Generate and test production deployment
The official deploy template creates .eas/workflows/deploy.yml. It fingerprints each platform, looks for a matching production build, then chooses Build + Submit or EAS Update.
eas workflow:create --template deploy eas workflow:run .eas/workflows/deploy.yml
Personal-project safety rule
Keep production manual until both platforms, environment values, and store connections work end to end.
8 Connect GitHub and turn on CI/CD
In the Expo dashboard, open the project’s GitHub settings, install the Expo GitHub app, and connect the matching repository. Then add this trigger near the top of .eas/workflows/deploy.yml:
name: Deploy to production
on:
push:
branches: ['main']git add .eas/workflows eas.json app.json app.config.* git commit -m "Add EAS CI/CD workflows" git push origin main
Add [eas skip], [skip eas], or [no eas] to a commit message when an eligible push should not run.
9 Operate the pipeline safely
- Use pull requests and local tests before merging to
main. - Review fingerprint and get-build jobs when EAS chooses an unexpected route.
- Keep fingerprint and update jobs on the same
productionenvironment as the build profile. - Use
eas update --rollout-percentage=10for a gradual OTA release. - Store submission does not complete store review and release.
eas workflow:list eas build:list --limit 5 eas update:list --branch production --limit 5
Get started ↗ · Production workflow ↗ · Workflow syntax ↗ · Workflow environments ↗
What EAS Update can change
The portrait cutaway shows the fixed native capabilities inside an installed app and the JavaScript layer that can change between store releases.
How I would stage a JavaScript update
This visual answers both parts: the commands that limit exposure and the evidence I would review before expanding it.