Configure code signing with CNG command
Run the command: npx expo-updates codesigning:configure --certificate-input-directory certs --key-input-directory ../keys. If using Continuous Native Generation (CNG), the app.json configuration changes are all that is needed and will be applied to native projects the next time they are generated.
Key rotation reasons
Key rotation is performed for three main reasons: key expiration (certificate validity ends), private key compromise (private key exposed publicly), or security best practices (periodic rotation). After key expiration, updates signed with the expired key will no longer be applied, though previously downloaded updates continue to function. A compromised key cannot guarantee update integrity.
Publish signed update command
Run: eas update --private-key-path ../keys/private-key.pem. The EAS CLI automatically detects code signing configuration and creates a digital signature using the private key. The signing process is performed locally so the private key never leaves the machine, and the generated signature is automatically sent to EAS.
Public key and certificate storage
The public key is not sensitive and may be stored alongside the private key. The code signing certificate should be included in the project and checked into source control. It contains the public key and a method for verifying code signatures.
Generate code signing key and certificate command
Run the command: npx expo-updates codesigning:generate --key-output-directory ../keys --certificate-output-directory certs --certificate-validity-duration-years 10 --certificate-common-name "Your Organization Name". This generates three files: ../keys/private-key.pem (the private key), ../keys/public-key.pem (the public key), and certs/certificate.pem (the code signing certificate).
Include message in EAS Update
CodePush CLI has a --description flag for including messages with updates. In EAS Update, you can achieve similar functionality using the extra field in your app config. Refer to the --message flag in the expo/UpdatesAPIDemo example.
Switch deployment at runtime in EAS Update
To switch the deployment being used at runtime in EAS Update (similar to CodePush's sync() function), use Updates.setUpdateURLAndRequestHeadersOverride(). This is documented in the Override update configuration at runtime guide.
EAS Update preview workflow
You can use EAS Update to achieve a web-like preview workflow. See the how to preview updates guide for details.
Branch promotion flow disadvantages
Disadvantages include: requires one channel per production runtime version to maintain historical updates for previous production releases, making runtime version policies more difficult to use. Requires bookkeeping of branch names and communication with team about which branches are currently pointed at test and production builds.
Branch promotion flow example channel and branch naming
Builds are created with channel names like 'production-rtv-1' to indicate the runtime version, and 'staging' for testing. Update branches are named like 'version-1' and 'version-2'. The 'staging' and 'production-rtv-1' channels are pointed at the appropriate update branches using the website or EAS CLI. When a new runtime version is needed, new builds are created with channels like 'production-rtv-2'.
Branch promotion flow runtime version requirements
The branch promotion flow requires manual specification of runtime versions and does not support automatic runtime version policies such as 'sdkVersion', 'appVersion', 'nativeVersion', and 'fingerprint'. Users must manually specify runtime versions when using this flow.
Branch promotion deployment flow overview
The branch promotion flow manages versioned releases using version-based update branches like 'version-1.0'. Builds are created per major version for production and separate builds for testing. Version-based update branches are dynamically mapped to channels to promote tested changes from testing to production. The flow uses expo-github-action to publish updates when merging to branches, with manual channel pointing to update branches via website or EAS CLI.
Custom expo-updates server as alternative to EAS Update
If your preferred release process is not supported by EAS Update, you can host your own update service compatible with the Expo Updates Protocol and point the expo-updates configuration to that service. The protocol only defines Runtime Version and Platform concepts, allowing you to create custom concepts on top, similar to how EAS Update built channels and branches.
Development builds can load updates from any channel
Development builds can load updates from any channel, provided the runtime version is compatible. This allows flexible preview and testing scenarios.
EAS Update concepts: channels, branches, and runtime versions
EAS Update provides channels to indicate which environment the update targets (such as 'production' or 'staging'), branches for Git integration and more complex workflows, and runtime versions to indicate which app version the update targets (such as '1.0.0' or '1.0.1'). For simple workflows, you can keep channels pointed at branches with the same name and mostly ignore branches.
Install expo-updates package
If expo-updates is not in package.json, install it using: npm: npx expo install expo-updates; yarn: yarn expo install expo-updates; pnpm: pnpm expo install expo-updates; bun: bun expo install expo-updates.
Inspect manifest via URL
To inspect a manifest, GET the URL from app.json updates.url (example: https://u.expo.dev/your-project-id) and add three query parameters: runtime-version, channel-name, and platform. Example: https://u.expo.dev/your-project-id?runtime-version=1.0.0&channel-name=production&platform=android. Going to this URL directly will show an error about a missing header, but adding the query parameters will display the manifest.
Use expo-dev-client for debugging updates
Create a development version of a build using expo-dev-client. This will help preview published updates inside a problematic build.
View network requests for debugging
To identify root cause of issues, inspect network requests the app is making to EAS servers. Use a program like Proxyman or Charles Proxy to watch requests. Follow the program's instructions for installing an SSL certificate to decode HTTPS requests. The requests of interest are from https://u.expo.dev (which returns update manifests) and https://assets.eascdn.net (which returns assets like images and fonts). Expect to see request headers: Expo-Runtime-Version (should match the runtime version of the build and update), expo-channel-name (should match the channel name in eas.json), and Expo-Platform (should be android or ios). Expect to see 200 response codes or 304 if nothing has changed.
Missing deployment indicates misconfiguration
If a deployment is not displayed, it means the build is not configured properly for EAS Update. To fix this, configure the channel, configure the runtime version, verify general configuration, and rebuild the app.
Enable expo-updates in debug builds with environment variable
By default, expo-updates is enabled only in release builds. To enable it in debug builds for testing, set the debug environment variable: export EX_UPDATES_NATIVE_DEBUG=1. This allows testing and debugging of native code in an environment closer to production without making a release build.
Missing updates in deployment
If the displayed deployment does not have any updates, an update must be published to the branch. If an update was already published, check the Updates page to make sure it matches the runtime version of the build.
Unexpected runtime version in deployment
If the deployment runtime version is unexpected, it means the build was not built with the correct runtime version. To fix this, configure the runtime version in app.json or app.config.js and rebuild the app.
Unexpected channel in deployment
If the deployment channel is unexpected, it means the build was not built with the correct channel. To fix this, configure the channel in eas.json and rebuild the app.
Monitoring update adoption via EAS dashboard
The details page for an update shows metrics for the number of users who have run the update, plus the number of failed installs. The deployments page includes a table and chart showing the number of users who have run each update related to a particular update channel and runtime version combination over a given time period.
Republish makes previous update immediately available
The republish command takes a previous update and publishes it again so it becomes the most current update on the branch. As users re-open their apps, they will see the newly re-published update and download it. This is similar to a Git reversion, where the correct commit is placed on top of the Git history.
Republish previous update command with branch name
Use the command 'eas update:republish --branch [branch-name]' to republish a previous update within a branch. This shows a list of recent updates on the branch and allows you to select the update group to republish. Example: 'eas update:republish --branch version-1.0'.
Republish previous update command with update group ID
Use the command 'eas update:republish --group [update-group-id]' to republish a previous update within a branch by its update group ID. Example: 'eas update:republish --group dbfd479f-d981-44ce-8774-f2fbcc386aa'.
Optimize bandwidth usage strategies
To optimize bandwidth usage: monitor usage metrics and identify unusual spikes or inefficiencies, optimize asset size by following the asset optimization guide, and use asset selection to reduce the number of assets included with each update (an advanced optimization).
Factors affecting bandwidth consumption
Actual bandwidth usage varies based on user behavior (not all users download every update; many skip intermediate updates when reopening the app) and missing assets (if an update includes fonts, images, or other assets not already on the device, they must be downloaded as well).
How to estimate update bandwidth size
To estimate bandwidth usage per update: identify the size of update assets not already on the device, then apply the compression ratio. For example, if JavaScript is 10 MB uncompressed and Hermes bytecode achieves a 2.6× compression ratio, the actual download size is approximately 3.85 MB.
Broken update recovery strategy: rollback considerations
When a broken update is published, the first thing to try is rolling back to an older update known to work. However, rollback may not be safe if the broken update modified persistent state (such as AsyncStorage or file system data) in a non-backwards-compatible way. Users should test rollback in a staging environment that closely emulates end user device state by loading the broken update and then rolling back.
Content appeared event distinction
expo-updates distinguishes between errors caught before and after the React Native native 'content appeared' event fires (ReactMarkerConstants.CONTENT_APPEARED on Android or RCTContentDidAppearNotification on iOS), which occurs approximately when the app's first view has rendered on screen for that update. This distinction is critical because if an error occurs before content has appeared, no persistent state modification code is assumed to have executed, making rollback safe.
Error recovery purpose and limitations
The error recovery flow in expo-updates is designed to prevent updates from bricking an app by ensuring the app has the opportunity to download a new update and fix itself. It is not a full safety net; users will still see crashes in many cases. The purpose is to prevent crashes on launch before the app can check for updates, making the app unusable until uninstalled and reinstalled.
iOS error stacktrace format
On iOS, when error recovery cannot recover, the stacktrace shows 'Last Exception Backtrace' and generally indicates an error originated in JavaScript, even though it appears thrown from expo-updates. The stacktrace includes references to EXUpdatesAppController and EXUpdatesErrorRecovery. However, Apple's crash reporting does not include the exception message detailing the underlying error and its JavaScript location, so reproducing the crash locally with Xcode debugger or macOS Console app may be necessary.
Android error stacktrace format
On Android, when an error recovery system cannot recover from a fatal JS error, it re-throws the original exception. The stacktrace shows 'AndroidRuntime: FATAL EXCEPTION: expo-updates-error-recovery' with the process name and will include 'com.facebook.react.common.JavascriptException'. The stacktrace of the original exception is preserved, and the underlying error information may be visible depending on the crash reporting service.
Fix forward strategy when rollback is unsafe
If no safe rollback update can be identified, the strategy is to fix it forward by publishing a new update with a fix as soon as possible (though not before being 100% confident in the fix). Even users who download the broken update should be able to download the fix through the error recovery mechanism, though they should take time to ensure the fix is solid.
Preview update URL format for development build
The URL format to open an EAS Update in a development build is: [slug]://expo-development-client/?url=[https://u.expo.dev/project-id]/group/[group-id]. Example: my-app://expo-development-client/?url=https://u.expo.dev/675cb1f0-fa3c-11e8-ac99-6374d9643cb2/group/47839bf2-9e01-467b-9378-4a978604ab11. Copy and paste this URL into the development build's launcher screen under Enter URL Manually, or create a QR code for the URL and scan it with your device's camera.
Platform property in builds
Every build has a platform, such as 'Android' or 'iOS'.
Update configuration is available
When the library checks for updates and when it downloads them, they are configurable via app config.
EAS Update with EAS Build recommended setup order
If planning to use EAS Update with EAS Build, follow the EAS Build setup guide first before setting up EAS Update. Alternatively, EAS Update can be used as a standalone service without other EAS services.
Testing updates after publication
After publishing an update, test it using: Extensions tab in a development build, Expo Orbit to install and launch the update, Updates API and app config to load updates programmatically, or manual testing by force closing and reopening a release build up to two times. Updates are automatically downloaded in the background when the app starts up and makes a request for new updates.
Prerequisites for EAS Update
EAS Update requires: an Expo account (free or paid plans available), a React Native project, Expo CLI and Expo Metro Config, and use of registerRootComponent instead of registerComponent (or registerApplication). Assets included in updates may not be available in release builds if registerRootComponent is not used.
Channel configuration in iOS native project
To configure update channels in Expo.plist without CNG, add EXUpdatesRequestHeaders key with a dictionary containing expo-channel-name key and the channel name as its value. The Expo.plist file must be added to the Xcode project.
Channel configuration in Android native project
To configure update channels in AndroidManifest.xml without CNG, add a meta-data entry with name expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY and android:value of {"expo-channel-name":"your-channel-name"}, replacing your-channel-name with the actual channel name.
iOS Expo.plist updates for EAS Update
When running eas update:configure on iOS projects without CNG, two entries are added to ios/project-name/Supporting/Expo.plist: EXUpdatesRuntimeVersion with value 1.0.0 (or similar), and EXUpdatesURL with value https://u.expo.dev/your-project-id. The Expo.plist file must be added to the Xcode project.
EAS setup version in GitHub Actions
The Setup EAS step should use expo/expo-github-action@v8 with eas-version: latest and pass the EXPO_TOKEN via the token parameter.
Setup Node GitHub Actions step for EAS workflow
The Setup Node step uses actions/setup-node@v6 and should include node-version: 24 and cache: yarn parameters.
Using Bun instead of Yarn in EAS Update workflow
To replace Yarn with Bun as the package manager, replace the 'Setup Node' step with 'Setup Bun' using 'uses: oven-sh/setup-bun@v1' with 'bun-version: latest'. Then replace the 'Install dependencies' step with 'run: bun install'.
Adding EXPO_TOKEN to GitHub repository secrets
Navigate to https://github.com/your-username/your-repo-name/settings/secrets/actions (replacing your-username and your-repo-name with your project's actual values). Under Repository secrets, click New repository secret. Create a secret named 'EXPO_TOKEN' and paste the personal access token as the value.
Creating EXPO_TOKEN personal access token
To set up EXPO_TOKEN for GitHub Actions, navigate to https://expo.dev/settings/access-tokens, click Create token to generate a new personal access token, and copy the generated token.
expo-github-action versions
The workflow uses expo/expo-github-action@v8 for the main setup and expo/expo-github-action/preview@v8 for the preview subaction.
Node.js version for EAS Update workflow
The GitHub Actions workflow should use Node.js version 24 with Yarn as the package manager, configured with the actions/setup-node@v6 action.
EXPO_TOKEN validation in GitHub Actions
The workflow must check for the presence of the EXPO_TOKEN secret at the beginning of the job. If the token is missing, the workflow should exit with status code 1 and display an error message.
Permissions required for GitHub Actions EAS workflow
The GitHub Actions workflow job requires the 'permissions' section to enable adding comments to pull requests. The required permissions are 'contents: read' and 'pull-requests: write'.
Checkout action version in EAS workflow
The workflow uses actions/checkout@v5 for checking out the repository.
Rebuild native code after migration
After configuring EAS Update, you must create new builds because the changes affect the native code layer inside builds. Once your builds are complete, you can start publishing updates.
Constants.manifest now returns null
Constants.manifest will now always return null after migrating to EAS Update. Access most properties using Constants.expoConfig from the expo-constants library instead.
Monthly active user billing for EAS Update
A monthly active user equals one unique installation of an app that downloads at least one update during a billing cycle. An app install downloading updates daily counts as 1 user. An install with no new updates counts as 0 users. Uninstalling and reinstalling counts as 2 users. A single device with two apps from one account is 2 users.
Image assets location and viewing
App users will download any new images or other assets when they detect a new update if those assets are not already part of their build. All assets uploaded to EAS servers are in dist/assets. The assets there are hashed with their extensions removed. To see a pretty-printed list of assets, run: npx expo export (or yarn expo export, pnpm expo export, or bun expo export depending on package manager).