Mobile app development has entered an era where agility and speed are paramount. Traditional methods, which require submitting new versions to app stores for review and awaiting user downloads, can lead to significant delays.
Over-the-air (OTA) updates provide a transformative solution by allowing you to push code changes directly to users’ devices, such as essential fixes, user interface improvements, and new features, thereby reducing downtime and enhancing the overall user experience.
In this article, we'll delve into how OTA updates work, discuss their benefits and challenges, and provide a step-by-step guide on implementing it within the Expo ecosystem.
In-Depth Analysis of OTA Updates: Capabilities, Benefits, and Limitations
📌 Capabilities of OTA Updates
Immediate Bug Fixes: Quickly resolve critical issues and deploy fixes without waiting for the app store review process. This minimizes downtime and reduces the risk of prolonged user impact.
Rapid Feature Delivery: Roll out new features in real time, enabling a more agile development cycle and gathering user feedback sooner. This accelerates the iterative process and improves overall product quality.
Swift UI/UX Tweaks (JS Side): Make minor adjustments to the user interface or experience directly in the JS bundle. Users receive these changes instantly on their next app launch, ensuring that the app stays up-to-date.
Instant Performance Improvements: Deploy performance enhancements immediately, directly benefiting the end-user experience without manual updates.
📌 Benefits of OTA Updates
Speed & Flexibility: Users get fixes and new features right away, and you can update as frequently as needed, improving your product continuously without waiting for app store reviews.
Enhanced User Experience: With OTA updates, users always have the latest version of your app. No need for them to do anything, which keeps the experience smooth and consistent.
Cost-Effective: By minimizing the need for frequent app store submissions, you save time, money and resources, freeing you up to concentrate on creating even better things.
Minimized Downtime: These updates happen silently in the background! So users can continue using the app without interruption, and they'll see the improvements the next time they open the app.
📌 Limitations of OTA Updates
Native Code Constraints: While OTA updates are great for JavaScript and assets, any changes that touches the native parts of your app still needs a full update through the app store.
Complexity in Rollback: While pushing updates is rapid, rolling back to a previous version with OTA can be more complex compared to regular app store updates. Proper version control is essential to mitigate this risk.
Initial Setup Overhead: Getting OTA updates up and running in the first place takes some careful setup and know-how. The initial setup can be time-consuming and may require ongoing maintenance to keep the update process smooth.
Limited Control Over User Update Timing: Since OTA updates are applied on the next app launch, users might continue using an older version for a while, which isn't ideal for urgent fixes.
Network Matters: The successful delivery of OTA updates depends on a reliable network connection. If the network is spotty, updates might be delayed or fail to download, leading to an inconsistent experience.
Security is Key: While OTA updates enhance security by enabling rapid patch deployment, they also require robust security measures to ensure the update process itself is not compromised.
Configure EAS Update
Prerequisites:
1- An Expo user account
2- Your React Native project for sure.
3- If your project is a bare React Native, it must use Expo CLI and extend the Expo Metro Config; 👉🏻 Installing Expo modules guide.
4- Your project must use the registerRootComponent function instead of registerComponent.
Now, follow these steps to set up EAS (Expo Application Services) Update:
1- Set up EAS Update
Install the Latest EAS CLI
EAS CLI is the command line app you will use to interact with EAS services from your terminal. Install it globally:
npm install --global eas-cli
Log in to Your Expo Account
If you are already signed in to an Expo account using Expo CLI, you can skip this step. If you are not, run the following command to log in:
- eas login
// then check whether you are logged:
- eas whoami
Configure Your Project
Navigate to your project directory in terminal, initialize EAS Update for your project by running:
eas update:configure
This command will update your app.json
file with the runtimeVersion
and updates.url
properties, and add the extra.eas.projectId
field if your project wasn't using any EAS services previously.
You'll see the following changes to your native projects:
➡️ Android
Inside the android/app/src/main/AndroidManifest.xml
file, you'll see the following additions:
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://u.expo.dev/your-project-id"/>
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
The EXPO_UPDATE_URL
value should contain your project's ID.
Inside android/app/src/main/res/values/strings.xml
, you'll see the expo_runtime_version
string entry in the resources object:
<resources>
<string name="app_name">MyApp</string>
+ <string name="expo_runtime_version">1.0.0</string>
</resources>
➡️ iOS
Inside ios/project-name/Supporting/Expo.plist
, you'll see the following additions:
<key>EXUpdatesRuntimeVersion</key>
<string>1.0.0</string>
<key>EXUpdatesURL</key>
<string>https://u.expo.dev/your-project-id</string>
The EXUpdatesURL
value should contain your project's ID.
2. Configuring a Channel
Manually
If you prefer not to use EAS Build for channel configuration, you can set the channel manually by updating both your AndroidManifest.xml and Expo.plist files.
➡️ Android Configuration
In your AndroidManifest.xml (located at android/app/src/main/AndroidManifest.xml), add the following meta-data tag:
<!--
This meta-data tag configures the Expo Updates module.
It tells the app to include a custom request header ("expo-channel-name")
with every update check, ensuring that the correct update channel is used.
-->
<meta-data android:name="expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY" android:value="{"expo-channel-name":"your-channel-name"}"/>
This line sets the custom channel (replace "your-channel-name" with your actual channel name) that the app will use when checking for updates.
➡️ iOS Configuration
For iOS, update your Expo.plist file (located at ios/your-project-name/Supporting/Expo.plist) with the following configuration:
<!--
This configuration in Expo.plist sets up the request headers for updates.
The key "expo-channel-name" is set to "your-channel-name", ensuring that the app
contacts the correct update channel on your server.
-->
<key>EXUpdatesRequestHeaders</key>
<dict>
<key>expo-channel-name</key>
<string>your-channel-name</string>
</dict>
This ensures that the Expo updates module on iOS uses the specified channel.
➡️ Alternative: Configuring via app.json
If you prefer to let Expo generate this configuration automatically, you can set the requestHeaders property in your app.json. For example:
{
"expo": {
"updates": {
// The URL where your app will fetch the OTA update manifest.
// This is an Expo hosted URL
"url": "<https://u.expo.dev/>...",
// Enables or disables OTA updates for the app.
// Set to true to allow updates; false will disable OTA functionality.
"enabled": true,
// Specifies the amount of time (in milliseconds) the app should wait
// for an update to be downloaded before falling back to a cached version.
// A value of 0 means the app will immediately use the cached update if available.
"fallbackToCacheTimeout": 0,
// Determines when the app checks for new updates.
// "ON_LOAD" instructs the app to check for updates every time it launches.
"checkAutomatically": "ON_LOAD",
// Additional HTTP headers that will be sent when the app requests an update.
// In this case, the "expo-channel-name" header is used to designate the update channel,
// allowing you to manage different release streams (e.g., test vs. production).
"requestHeaders": {
"expo-channel-name": "your-channel-name"
}
}
}
}
Explanation:
- "url" points to your manifest endpoint.
- "fallbackToCacheTimeout": 0 ensures that if the update isn’t immediately available, the app won’t wait.
- "checkAutomatically": "ON_LOAD" instructs the app to check for updates each time it launches.
- The "requestHeaders" property allows you to specify a channel (e.g., test or production).
Then run:
npx expo prebuild
This command will inject the proper configuration into both AndroidManifest.xml and Expo.plist.
Note: Typically, you might use two channels: test and production.
3. EAS Update Command Reference
Below is a list of commonly used commands for managing updates, channels, and branches.
Manage updates
Create and Publish a New Update:
eas update --branch [branch-name] --message "Your update message"
Example:
eas update --branch version-1.0 --message "Fixes typo"
View a Specific Update:
eas update:view [update-group-id]
Rollback an Update
Rollback to a Previous Update:
eas update:rollback
Republish a Previous Update Within a Branch
Republish by Group ID:
eas update:republish --group [update-group-id]
Republish by Branch:
eas update:republish --branch [branch-name]
Examples:
eas update:republish --group dbfd479f-d981-44ce-8774-f2fbcc386aa
eas update:republish --branch version-1.0
Manage Channels and Branches
In EAS Update, a channel acts as the "distribution pipeline" that delivers updates to your app, while a branch represents the mode or version stream—such as test or production—from which updates are sourced.
By linking a branch to a channel, you control which updates reach specific user groups. For example:
- A test branch connected to a test channel delivers updates to internal or beta testers.
- A production branch tied to a production channel sends updates to live users. This separation ensures experimental changes stay confined to testing environments, protecting your production audience while enabling rapid iteration and feedback in development.
Channels:
View a Channel:
eas channel:view production
List All Channels:
eas channel:list
Branches:
List All Branches:
eas branch:list
View a Specific Branch and Its Updates:
eas branch:view version-1.0
Delete a Branch:
eas branch:delete [branch-name]
Rename a Branch:
eas branch:rename --from version-1.0 --to version-1.0-new
4. Add a hook to check for updates
it's optional since you set "checkAutomatically": "ON_LOAD",
.
import { useEffect } from "react";
import * as Updates from "expo-updates";
export function useisHasUpdates(): void {
useEffect(() => {
async function update(): Promise<void> {
try {
const { isAvailable } = await Updates.checkForUpdateAsync();
if (isAvailable) {
await Updates.fetchUpdateAsync();
}
} catch {
console.error("Error; checking for updates");
}
}
if (!__DEV__) {
update();
}
}, []);
}
5. Create a build for your project
Generate a production build with the runtime version and channel name, APK for Android and TestFlight for iOS.
6. Make changes locally
After creating the build, you are ready to iterate on the project; make any desired changes to your project's JS, styling, or image assets.
7. publish an update
Use the eas update
command, and specify a name for the channel and a message to describe the update, as we talked before.
📱 But how does publishing an update work?
When you publish an update with the eas update
command, it generates a new update bundle and uploads it to the EAS servers.
then it locates the correct branch to publish a new update according to channel name. It is similar to how Git commit works, where every commit is on a Git branch.
8. Test
You can manually test the update by force closing and reopening a release build of your app up to two times to download and apply the update.
Updates for non-development builds are automatically downloaded to the device in the background when the app starts up and makes a request for any new updates. The update will be applied after it is downloaded and the app is restarted.
And Voilà, you are done now!! 🥂
In some cases more control of how updates are sent to an app may be needed, and one option is to implement a custom updates server that adheres to the specification in order to serve update manifests and assets. 👇🏻
Configure a Custom Expo Updates Server
You can set up your own custom Expo updates server to gain complete control over your app’s update distribution. Below are the implementation steps:
Implementation Steps
- Create a Custom Server and Implement Update Endpoints
Initialize the Project:
Clone the official repository for the custom Expo updates server:
git clone https://github.com/expo/custom-expo-updates-server.git
cd custom-expo-updates-server
This repo contains a basic server implementation built with Next.js. The key API endpoints are located in:
- pages/api/manifest.js – Handles the manifest file generation.
- pages/api/assets.js – Serves the asset bundles.
These endpoints adhere to the Expo Updates protocol, allowing your app to fetch the latest update bundles.
- Set Up EAS Update Code Signing
💡 Code Signing:
To ensure your updates are secure and verifiable, set up code signing by following the Expo documentation on EAS Update Code Signing.
This involves generating a signing certificate and configuring your server and app to use it, which protects your update bundles from tampering.
💡 Uploading Credentials Manually
To upload credentials from your local credentials.json file and have them managed by EAS, follow these steps:
- Open your terminal at the root directory of your project.
- Run the command:
eas credentials
- When prompted, select the desired platform.
- Choose the option: "Credentials.json: Upload/Download credentials between EAS servers and your local json".
- Then select: "Upload credentials from credentials.json to EAS".
- Repeat the process for another platform if needed by running the command again.
This approach ensures that your credentials are safely managed by EAS, streamlining your build and deployment process.
- Configure Your React Native Mobile App
- Update the Configuration in app.json
{
"expo": {
"updates": {
// This can be an Expo hosted URL or your custom server's endpoint.
"url": "https://your-custom-server.com/manifest/",
"enabled": true,
"fallbackToCacheTimeout": 0,
"checkAutomatically": "ON_LOAD",
// Path to the code signing certificate that is used to verify the authenticity
// of the OTA update bundle. This file should be placed in your project’s directory.
"codeSigningCertificate": "./certs/certificate.pem",
// Metadata related to the code signing process.
// "keyid" should correspond to the identifier of the signing key.
// "alg" specifies the cryptographic algorithm used for signing (e.g., RSA with SHA-256).
"codeSigningMetadata": {
"keyid": "main",
"alg": "rsa-v1_5-sha256"
},
"requestHeaders": {
"expo-channel-name": "channel"
}
}
}
}
Explanation:
- The code signing fields secure your updates.
Tip: If you run npx expo prebuild after setting the requestHeaders property, the necessary configuration will be automatically injected into both AndroidManifest.xml and Expo.plist.
- Add Scripts to Upload Update Bundles
{
"scripts": {
"expo-publish-alt": "node ./scripts/publish.js",
"expo-rollback-alt": "node ./scripts/rollback.js"
}
}
These scripts handle the uploading of new update bundles to your custom server. In this setup, while the publish script worked properly, the rollout (gradual update distribution) wasn’t implemented on the server side. After understanding channels and branches management, you may choose to migrate to EAS Update without relying on EAS Build if that better fits your workflow.
💾 Setting up a custom Expo updates server gives you granular control over update distribution, code signing, and rollout strategies. By manually configuring both your server and your React Native app, you can tailor the OTA update process to your specific requirements. However, as you refine your approach, you might find that the channel and branch management capabilities provided by EAS Update offer a more streamlined solution—especially if you decide not to use EAS Build.
📚 Additional resources:
👉 Expo OTA Updates Documentation
👉 Expo EAS Update Documentation
👉 Secure your update bundles by following best practices for Expo Code Signing for OTA Updates
👉 Custom Expo Updates Server Repository
Summary
Over-the-air (OTA) updates through Expo's EAS Update revolutionize mobile app development by enabling rapid deployment of bug fixes, performance optimizations, and new features, bypassing the delays of traditional app store reviews. By following best practices, such as effective channel and branch management, secure code signing, and rigorous testing, you can mitigate OTA limitations and deliver a seamless, high-quality user experience.
Happy Building! Great solutions power exceptional experiences. 🚀
Top comments (0)