# How Turbopush Works (/docs/concepts) ## What are OTA Updates? Over-The-Air (OTA) updates let you push new code directly to users' devices without going through the App Store or Google Play review process. Instead of submitting a new binary and waiting days for approval, you deploy changes and users receive them automatically—often within minutes. React Native apps are uniquely suited for OTA updates because they consist of two distinct layers: - **Native shell** — compiled code (Swift, Kotlin, Objective-C, Java) that runs on the device - **JavaScript bundle** — your React components, business logic, and UI code The JavaScript bundle is loaded at runtime by the native shell. Turbopush replaces this bundle on-device with a newer version without touching the native layer. ## What Can and Cannot Be Updated OTA | Can update | Cannot update | |---|---| | JavaScript code and logic | Native modules (Swift, Kotlin) | | React components and screens | New native dependencies | | Styles and layouts | App permissions and capabilities | | Images and assets bundled with JS | App icon, name, splash screen | > OTA updates that add or modify native code require a full app store release. Turbopush only updates the JavaScript bundle. ## How an Update Reaches a Device 1. **Release** — You run `turbopush release-react` or `turbopush release-expo`. The CLI bundles your JS code and uploads it to Turbopush servers. 2. **Check** — When the app launches (or resumes, depending on your config), the Turbopush SDK checks the API for a newer release compatible with the app's current binary version. 3. **Download** — If an update is available and the device is eligible (based on rollout percentage), it downloads silently in the background. 4. **Install** — The update is applied according to the configured `installMode`: immediately, on next restart, or on next resume. 5. **Rollback** — If the new bundle crashes on startup, the SDK automatically reverts to the previous working version. ## Key Concepts ### Apps and Deployments An **app** in Turbopush represents a single platform build of your React Native project. Because iOS and Android bundles have different contents, you create one app per platform: ``` MyApp-iOS ← iOS deployments MyApp-Android ← Android deployments ``` Each app has **deployments** — named release channels. Every app starts with `Staging` and `Production`. You can create additional deployments (e.g. `Beta`) for more granular control. ### Releases and Labels A **release** is a versioned JavaScript bundle uploaded to a deployment. Releases are immutable — you cannot edit their code after publishing. Each release gets an auto-incremented label (`v1`, `v2`, `v3`, ...). Each release has two version constraints: - **Target binary version** — The native app version(s) this release is compatible with. Supports semver ranges (e.g. `>=1.2.0 <2.0.0`). - **Deployment** — Which channel (`Staging`, `Production`, etc.) the release belongs to. ### Mandatory vs Optional Updates - **Optional** (default) — Users download the update silently and it applies on next restart. - **Mandatory** — The update is installed and the app restarts immediately, regardless of `installMode`. Use this for critical bug fixes. ### Rollout Percentage Instead of releasing to 100% of users at once, you can target a percentage: ```bash turbopush release-react myapp-ios ios --rollout 10 ``` This sends the update to a random 10% of eligible devices. Once you're confident the release is stable, increase the percentage with `turbopush patch`. ### Promoting Releases The recommended workflow is to release to `Staging`, test, then **promote** to `Production`. Promotion copies the exact same bundle — no repackaging, no risk of differences between environments: ```bash turbopush promote myapp-ios Staging Production ``` ### Rollback If a release causes issues, rollback creates a new release using the code from the previous version: ```bash turbopush rollback myapp-ios Production ``` Users on the broken version receive the rollback update automatically. The release history is preserved — rollback adds a new entry rather than deleting anything. ## Install Modes Control when an update is applied using `installMode` in your SDK configuration: | Mode | Behavior | |---|---| | `IMMEDIATE` | Restart the app right after download | | `ON_NEXT_RESTART` | Apply on the next natural app restart *(default)* | | `ON_NEXT_RESUME` | Apply when the app comes back from background | | `ON_NEXT_SUSPEND` | Apply while the app is backgrounded | ## Automatic Rollback Safety If a downloaded update causes the app to crash on startup, the Turbopush SDK detects this and automatically reverts to the previous bundle. The server records the rollback as a metric, visible in `turbopush deployment ls` and in the Turbopush dashboard. # Getting Started (/docs) Welcome to Turbopush! This guide will teach you how to do the initial Turbopush setup and perform your first release. > We have a [**pre-built Expo example app**](/docs/examples/expo) ready for you to test OTA updates in minutes, without any setup. Just scan a QR code and start testing! ## 1. CLI Installation First, install the Turbopush CLI in your project: ```bash pnpm add -D @turbopush/cli ``` Verify the installation was successful: ```bash turbopush --version ``` ## 2. Account Creation If you don't have an account yet, create one at [https://app.turbopush.org/sign-up](https://app.turbopush.org/sign-up). ## 3. Authentication To start using Turbopush, you need to log in: ```bash turbopush login ``` This command will: 1. Open your browser for authentication 2. Generate an access key 3. Prompt you to paste the key in the terminal To verify you're logged in: ```bash turbopush whoami ``` 3.1 Set your default organization ```bash turbopush org set ``` If you don't know your organization slug, you can list all your organizations with: ```bash turbopush org list ``` ## 4. Registering Your Application > Create separate apps for iOS and Android, this ensures each platform has appropriate update packages. #### iOS ```bash turbopush app add MyApp-iOS --appSlug myapp-ios ``` #### Android ```bash turbopush app add MyApp-Android --appSlug myapp-android ``` > The `appSlug` is used to identify the app in the CLI and API for publishing and managing releases. If not provided, the slug will be generated from the app name. #### After creating the app The command will return deployment keys for `Staging` and `Production`. Write down these keys, you'll need them to configure your React Native app, example: ```bash ┌────────────┬─────────────────────────────────────┐ │ Name │ Deployment Key │ ├────────────┼─────────────────────────────────────┤ │ Production │ dk_19ffa8fc2a91f2cd9afb3bfecafef06d │ ├────────────┼─────────────────────────────────────┤ │ Staging │ dk_5g5dc7aa6197c1f292cd12ca28f39bb3 │ └────────────┴─────────────────────────────────────┘ ``` ## 5. SDK Configuration **React Native** ### Installation ```bash pnpm add @turbopush/react-native-code-push ``` ### iOS Setup 1. Run `cd ios && pod install && cd ..` to install all the necessary CocoaPods dependencies. ​ 2. Change bundleUrl on AppDelegate file. **Swift** ```swift title="ios/[ProjectName]/AppDelegate.swift" import React import CodePush // [!code ++] override func bundleURL() -> URL? { #if DEBUG RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") #else Bundle.main.url(forResource: "main", withExtension: "jsbundle") // [!code --] CodePush.bundleURL() // [!code ++] #endif } ``` **Objective C** ```objc title="ios/[ProjectName]/AppDelegate.m" #import "AppDelegate.h" #import // [!code ++] - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; // [!code --] return [CodePush bundleURL]; // [!code ++] #endif } ``` 4. Add the CodePushDeploymentKey key (generated in the create app step): ```xml title="ios/[ProjectName]/Info.plist" CodePushDeploymentKey YOUR_IOS_DEPLOYMENT_KEY ``` 5. Certifies that your deployment_target is 15.5 or higher: ```kotlin title="ios/Podfile" platform :ios, '15.5' ``` ### Android Setup 1. Add this line at the end of the file: ```kotlin title="android/app/build.gradle" apply from: "../../node_modules/@turbopush/react-native-code-push/android/codepush.gradle" ``` 2. Update the `MainApplication` file to use CodePush with the following changes: **Kotlin Rn82** ```kotlin title="android/app/src/main/java/com//MainApplication.kt" import com.facebook.react.ReactHost import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.microsoft.codepush.react.CodePush // [!code ++] class MainApplication : Application(), ReactApplication { override val reactHost: ReactHost by lazy { getDefaultReactHost( context = applicationContext, packageList = PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: // add(MyReactNativePackage()) }, jsBundleFilePath = CodePush.getJSBundleFile(), // [!code ++] ) } override fun onCreate() { super.onCreate() val deploymentKey = getString(R.string.CodePushDeploymentKey) // [!code ++] CodePush.getInstance(deploymentKey, this, BuildConfig.DEBUG) // [!code ++] loadReactNative(this) } } ``` **Kotlin** ```kotlin title="android/app/src/main/java/com//MainApplication.kt" import com.facebook.react.ReactHost import com.facebook.soloader.SoLoader import com.microsoft.codepush.react.CodePush // [!code ++] class MainApplication : Application(), ReactApplication { override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { override fun getJSBundleFile(): String { // [!code ++] return CodePush.getJSBundleFile() // [!code ++] } // [!code ++] }; override fun onCreate() { super.onCreate() val deploymentKey = getString(R.string.CodePushDeploymentKey) // [!code ++] CodePush.getInstance(deploymentKey, this, BuildConfig.DEBUG) // [!code ++] loadReactNative(this) } } ``` 3. Add the CodePushDeploymentKey key (generated in the create app step): ```xml title="android/app/src/main/res/values/strings.xml" YOUR_ANDROID_DEPLOYMENT_KEY ``` ### Basic JavaScript configuration ```javascript title="App.tsx" import codePush from "@turbopush/react-native-code-push"; // [!code ++] const App = () => { // Your app component }; export default App; // [!code --] export default codePush(App); // [!code ++] ``` For advanced configuration, refer to the [JavaScript API Reference](/docs/setup/api-js). ## 6. First Release Creating a Release (React Native) For React Native, use the specific command that automatically generates the bundle: ```bash turbopush release-react myapp-ios ios -t "1.0.0" ``` or ```bash turbopush release-react myapp-android android -t "1.0.0" ``` ### Important release parameters | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `--deploymentName`, `-d` | No | `Staging` | Specifies the deployment | | `--description`, `--des` | No | - | Description of changes | | `--mandatory`, `-m` | No | `false` | Marks as mandatory update | | `--rollout`, `-r` | No | `100` | Sets rollout percentage | | `--targetBinaryVersion`, `-t` | No | auto-detected | Sets the target binary version | ### Example with parameters ```bash turbopush release-react myapp-ios ios \\ --deploymentName Production \\ --description "Critical bug fixes" \\ --mandatory \\ --rollout 50 \\ --targetBinaryVersion "1.0.0" ``` Read more about [Releasing Updates](/docs/cli/releasing-updates/react-native). **Expo** ### Installation ```bash pnpm add @turbopush/react-native-code-push @turbopush/turbopush-expo-plugin expo-build-properties ``` ### Expo Plugin Setup 1. Add the following to your `app.json` or `app.config.ts` file: > Turbopush SDK requires a minimum deployment target of 15.5, if you're using a higher version, you can ignore the expo-build-properties plugin. ```json { "plugins": [ [ "expo-build-properties", { "ios": { "deploymentTarget": "15.5", }, }, ], [ "@turbopush/turbopush-expo-plugin", { "android": { "CodePushDeploymentKey": "YOUR_ANDROID_CODE_PUSH_KEY", // generated in the create app step }, "ios": { "CodePushDeploymentKey": "YOUR_IOS_CODE_PUSH_KEY", // generated in the create app step }, } ] ] } ``` > For the sake of simplicity, we're using the deployment key hardcoded in the `app.json` file. However, we recommend using environment variables to store your deployment keys. e.g. `process.env.EXPO_PUBLIC_TURBOPUSH_ANDROID_KEY` and `process.env.EXPO_PUBLIC_TURBOPUSH_IOS_KEY`. ### Prebuild Run this command to generate the native code for your app. ```bash expo prebuild ``` ### Basic JavaScript configuration In your main app file: ```javascript title="App.tsx" import codePush from "@turbopush/react-native-code-push"; // [!code ++] const App = () => { // Your app component }; export default App; // [!code --] export default codePush(App); // [!code ++] ``` For advanced configuration, refer to the [JavaScript API Reference](/docs/setup/api-js). ## 6. First Release Creating a Release (Expo) Use the specific command that automatically generates the bundle: ```bash turbopush release-expo myapp-ios ios "1.0.0" ``` or ```bash turbopush release-expo myapp-android android "1.0.0" ``` ### Important release parameters | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `--deploymentName`, `-d` | No | `Staging` | Specifies the deployment | | `--description`, `--des` | No | - | Description of changes | | `--mandatory`, `-m` | No | `false` | Marks as mandatory update | | `--rollout`, `-r` | No | `100` | Sets rollout percentage | ### Example with parameters ```bash turbopush release-expo myapp-ios ios "1.0.0" \\ --deploymentName "Production" \\ --description "Critical bug fixes" \\ --mandatory \\ --rollout 50 ``` Read more about [Releasing Updates](/docs/cli/releasing-updates/expo). ## Troubleshooting ### Binary version error Make sure the version specified in the release corresponds to the version in `Info.plist` (iOS) or `build.gradle` (Android). ### App not receiving updates 1. Verify deployment keys are correct 2. Make sure `codePush.sync()` is being called 3. Check device logs Now you're ready to start using Turbopush! ### For version 0.75 and below or CodePushServerURL usage For version 0.75 and below, you need to use the old `react-native-code-push` instead of the new `@turbopush/react-native-code-push` package. > This is not supported in expo projects. ```bash pnpm add react-native-code-push ``` In `ios/[ProjectName]/Info.plist`, you need to add the following: ```xml CodePushServerURL https://api.turbopush.org ``` And in `android/app/src/main/res/values/strings.xml`, you need to add the following: ```xml https://api.turbopush.org ``` # Account Management (/docs/cli/account-management) Create your account at https://app.turbopush.org/sign-up ## Authentication Login to use CLI commands: ```bash turbopush login ``` This opens a browser for authentication. Copy the generated access key back to the CLI. Check current session: ```bash turbopush whoami ``` To end your current session: ```bash turbopush logout ``` ## Access Keys For CI/CD environments where browser login isn't possible, create an access key in the dashboard, then: ```bash turbopush login --accessKey ``` This key persists across logout and can be reused until manually revoked or expired. # App Management (/docs/cli/app-management) ## Creating an App Before you can deploy any updates, you need to register an app with the Turbopush service using the following command: ```bash turbopush app add --appSlug ``` The `appSlug` identifies your app in CLI commands and deployment keys. If omitted, it's derived from the app name. > Create separate apps per platform (e.g., `MyApp-iOS`, `MyApp-Android`). Mixing platforms causes installation issues due to different bundle contents. Every new app comes with `Staging` and `Production` deployments. The CLI outputs their deployment keys for SDK configuration. ### Delete App ```bash turbopush app rm ``` ### List All Apps ```bash turbopush app ls ``` ## Deployments Deployments are release channels within an app. Default: `Staging` and `Production`. ### Create Deployment ```bash turbopush deployment add ``` ### Delete Deployment ```bash turbopush deployment rm ``` ### List Deployments Use `-k` to show deployment keys. ```bash turbopush deployment ls -k ``` ### Metrics | Metric | Description | |--------|-------------| | **Active** | Users currently running this release | | **Total** | All successful installs ever | | **Pending** | Downloaded but not yet installed | | **Rollbacks** | Automatic client rollbacks (crash detection) | | **Rollout** | % of users eligible for this update | | **Disabled** | If true, update isn't downloadable | "No installs recorded" means no telemetry received yet. # Code Signing (/docs/cli/code-signing) Code signing creates digital signatures for bundles, verified on client before installation. Prevents man-in-the-middle attacks. ## How It Works First, the developer generates an asymmetric key pair: the private key will be used for signing bundles; the public key for bundle signature verification. The Turbopush CLI then uses the private key to sign bundles during `release-react` command. The public key is shipped with the mobile application. Control over the generation and management of keys is in the hands of the developer. At the end of release command, the cli computes the bundle's content hash and places this value into a JWT signed with the private key. When the Turbopush plugin downloads a bundle to a device, it checks the release file containing the JWT and validates the JWT signature using the public key. If validation fails, the update is not installed. ## Key Generation ```shell openssl genrsa -out private.pem openssl rsa -pubout -in private.pem -out public.pem ``` The **private key** (`private.pem`) is kept on your machine and passed to the CLI when releasing. The **public key** (`public.pem`) is shipped inside the app binary. ## Setup ### Expo (managed projects) If you use Expo, configure the public key through the Turbopush config plugin instead of editing native files manually. Add `CodePushPublicKey` alongside `CodePushDeploymentKey` in your `app.json` / `app.config.ts`: ```json { "plugins": [ [ "@turbopush/turbopush-expo-plugin", { "android": { "CodePushDeploymentKey": "YOUR_ANDROID_CODE_PUSH_KEY", "CodePushPublicKey": "YOUR_PUBLIC_KEY" }, "ios": { "CodePushDeploymentKey": "YOUR_IOS_CODE_PUSH_KEY", "CodePushPublicKey": "YOUR_PUBLIC_KEY" } } ] ] } ``` During `expo prebuild`, the plugin injects `CodePushPublicKey` into `Info.plist` (iOS) and `strings.xml` (Android). > The SDK accepts the public key with or without the `-----BEGIN PUBLIC KEY-----` / `-----END PUBLIC KEY-----` headers, it strips both before decoding. You can paste the full `public.pem` content as-is. The private key used to sign releases is passed to the CLI with `--privateKeyPath` (or `-k`) — it is **not** part of the plugin config. ### Bare React Native Add the public key to your native code. The SDK reads it from the app config at runtime, so no JS change is needed: **Android** — `android/app/src/main/res/values/strings.xml`: ```xml my_app -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY----- ``` **iOS** — `ios/MyApp/Info.plist`: ```xml CodePushPublicKey -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY----- ``` > **Note:** Before releasing a signed update, make sure the targeting version of your app already includes the public key configuration. Otherwise, devices running older versions will not be able to install signed updates, as they cannot validate the signature. ## Releasing Sign the update by passing the private key with `--privateKeyPath` (or `-k`). It works with both release commands: ```shell # Bare React Native turbopush release-react myapp-ios ios -k ./private.pem # Expo managed projects turbopush release-expo myapp-ios ios "1.0.0" -k ./private.pem ``` ## FAQ | Scenario | Result | |----------|--------| | Updated CLI but don't want code signing | No impact. Feature is optional. | | Public key configured but forgot to sign release | Update rejected. Release again with private key. | | Signed update but app has outdated/no public key | Update rejected. Ensure app has matching public key. | | Lost private key | Generate new key pair, release new binary with new public key, then release updates with new private key. | # Debugging (/docs/cli/debugging) View Turbopush plugin logs from your running app, this is useful if you run into an issue and want to understand why: ### iOS Simulator ```bash turbopush debug ios ``` ### Android Emulator or Device ```bash turbopush debug android ``` Turbopush debug console output showing plugin logs on an Android device Uses iOS system logs and ADB logcat under the hood, filtered for Turbopush entries. > Currently supports iOS simulator only (not physical devices). Android supports both emulators and devices. # Patching Update Metadata (/docs/cli/patching-updates) Modify metadata of an existing release without changing the code, for example, if you forgot to mark a critical bug fix as mandatory or you want to increase the rollout percentage of an update: ```bash turbopush patch ``` > To fix broken code, use [rollback](./rolling-back-updates) instead, then release a new update. ## Examples ### Mark Latest Release as Mandatory ```bash turbopush patch myapp-ios Production -m ``` ### Increase Rollout Percentage ```bash turbopush patch myapp-ios Production -l v23 --rollout 50% ``` ## Parameters | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `appSlug` | Yes | - | The app slug | | `deploymentName` | Yes | - | The deployment name (e.g., `Staging`, `Production`) | | `--label`, `-l` | No | latest | Target a specific release. Find labels via `deployment history` | | `--mandatory`, `-m` | No | - | Set as mandatory. Use `--mandatory false` to make optional | | `--description`, `--des` | No | - | Update release description | | `--disabled`, `-x` | No | - | Disable/enable downloads. Use `--disabled false` to re-enable | | `--rollout`, `-r` | No | - | Increase rollout % | | `--targetBinaryVersion`, `-t` | No | - | Change compatible binary versions (semver range) | # Promoting Updates (/docs/cli/promoting-updates) Copy a release from one deployment to another (e.g., Staging → Production): ```bash turbopush promote ``` ## Why Promote? 1. **Faster** — No need to reassemble release assets 2. **Safer** — Exact same code you tested gets deployed **Recommended workflow:** Release to `Staging` → Test → Promote to `Production` ## Example ```bash turbopush promote myapp-ios Staging Production -t "*" ``` ## Parameters | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `appSlug` | Yes | - | The app slug | | `sourceDeploymentName` | Yes | - | Source deployment (e.g., `Staging`) | | `destDeploymentName` | Yes | - | Destination deployment (e.g., `Production`) | | `--label`, `-l` | No | latest | Specific release to promote | | `--description`, `--des` | No | inherited | Change log | | `--disabled`, `-x` | No | inherited | Prevent downloads | | `--mandatory`, `-m` | No | inherited | Force update | | `--rollout`, `-r` | No | 100 | % of users (not inherited, must be explicit) | | `--targetBinaryVersion`, `-t` | No | inherited | Compatible binary versions | | `--noDuplicateReleaseError` | No | `false` | Warn instead of error on duplicate | Optional parameters override values from the source release. If omitted, values are inherited. # Release History (/docs/cli/release-history) ## View History ```bash turbopush deployment history ``` Shows the 50 most recent releases with labels, metadata, and install metrics. Promotions and rollbacks are indicated. ```bash Label Release Time App Version Mandatory Description v1 35 minutes ago 1.0.0 No v2 35 minutes ago 1.0.0 No (Promoted v1 from "Staging") v3 32 minutes ago 1.0.0 No This is a stable release. v4 29 minutes ago 1.0.0 No This is a broken release :( v5 22 minutes ago 1.0.0 No This is a stable release. (Rolled back v4 to v3) ``` Add `--displayAuthor` (or `-a`) to see who released each update. **Alias:** `npx turbopush deployment h` ## Clear History ```bash turbopush deployment clear ``` > Irreversible. Devices using this deployment key will no longer receive cleared updates. # Rolling Back Updates (/docs/cli/rolling-back-updates) A deployment's release history is immutable, so you cannot delete or remove individual updates once they have been released without deleting all of the deployment's release history. However, if you release an update that is broken or contains unintended features, it is easy to roll it back using the `rollback` command: ### Syntax ```bash turbopush rollback ``` ### Example ```bash turbopush rollback myapp-ios Production ``` ## How It Works Rollback creates a new release with the **exact code and metadata** from the previous version. | Release | Description | Mandatory | |---------|-------------|-----------| | v1 | Initial release | Yes | | v2 | New feature | No | | v3 | Bug fixes | Yes | | v4 *(rollback)* | New feature | No | After rollback: users on v3 get "moved back" to v2's code. Users already on v2 see no update (same hash). ## Target a Specific Release ```bash turbopush rollback myapp-ios Production --targetRelease v34 ``` Rollbacks are marked in `deployment history` output. # Blog (/docs/blog) ## Articles - [What is CodePush?](/docs/blog/what-is-codepush) - [Migrating from AppCenter CodePush to Turbopush](/docs/blog/migrating-from-appcenter-codepush-to-turbopush) - [Ship Expo Updates (OTA) in Minutes](/docs/blog/ship-expo-updates-in-minutes) - [What Are OTA Updates and Why They Matter](/docs/blog/what-are-ota-updates-and-why-they-matter) - [Web Deploys vs Mobile Releases: Why Shipping Mobile Is Still Stuck in 2010](/docs/blog/web-deploys-vs-mobile-releases) # Migrate AppCenter CodePush to Turbopush — Complete Guide (/docs/blog/migrating-from-appcenter-codepush-to-turbopush) If you're reading this, you're likely one of the thousands of developers affected by Microsoft's decision to shut down AppCenter CodePush on March 31, 2025. Don't panic—migrating to Turbopush is straightforward, and you can complete it in under 30 minutes. **The good news?** Turbopush is 100% CodePush compatible. Your existing JavaScript code using the CodePush SDK requires minimal changes, and your deployment workflow stays virtually the same. This guide will walk you through migrating your React Native app from AppCenter CodePush to Turbopush, step by step. ## Why Migrate to Turbopush? Before we dive into the technical steps, let's address the elephant in the room: **why Turbopush instead of self-hosting?** When AppCenter announced its shutdown, many teams considered self-hosting alternatives like code-push-server or building their own infrastructure. Here's why that's often not the best choice: ### Turbopush vs Self-Hosted: A Comparison | Feature | Turbopush | Self-Hosted | |---------|-----------|-------------| | **Setup Time** | ~10 minutes | Days to weeks | | **Infrastructure Management** | Zero—we handle it | You're responsible for servers, databases, storage | | **Global CDN** | ✅ Built-in, worldwide | ❌ You need to configure and pay for CDN | | **Scalability** | ✅ Automatic | ❌ Manual scaling, potential downtime | | **Security Updates** | ✅ Automatic | ❌ You must patch and maintain | | **SSL/TLS Certificates** | ✅ Managed | ❌ Manual renewal and configuration | | **Monitoring & Analytics** | ✅ Built-in dashboard | ❌ Build or integrate your own | | **Support** | ✅ Dedicated team | ❌ You're on your own | | **Cost** | Predictable monthly fee | Hidden costs: servers, bandwidth, DevOps time | **The bottom line:** Self-hosting might seem "free," but the hidden costs of infrastructure, maintenance, security, and DevOps time quickly add up. Most teams find that Turbopush pays for itself in the first month just from saved engineering hours. --- ## Step 1: Create Your Turbopush Account Head over to [https://app.turbopush.org/sign-up](https://app.turbopush.org/sign-up?utm_source=blog&utm_campaign=migrating-from-appcenter-codepush-to-turbopush) and create your free account. The setup takes less than a minute. ## Step 2: Install the Turbopush CLI Remove the old AppCenter CLI (if installed globally): ```bash pnpm remove appcenter-cli ``` Then install the Turbopush CLI in your project: ```bash pnpm add -D @turbopush/cli ``` Verify the installation: ```bash turbopush --version ``` ## Step 3: Authenticate with Turbopush Log in to your new Turbopush account: ```bash turbopush login ``` This will: 1. Open your browser for authentication 2. Generate a secure access key 3. Prompt you to paste the key in your terminal Verify you're logged in: ```bash turbopush whoami ``` ### Set Your Default Organization ```bash turbopush org set ``` Not sure what your organization slug is? List them all: ```bash turbopush org list ``` ## Step 4: Register Your Apps in Turbopush Create separate apps for iOS and Android (just like you had in AppCenter): #### iOS ```bash turbopush app add MyApp-iOS --appSlug myapp-ios ``` #### Android ```bash turbopush app add MyApp-Android --appSlug myapp-android ``` After creating each app, you'll receive deployment keys: ```bash ┌────────────┬─────────────────────────────────────┐ │ Name │ Deployment Key │ ├────────────┼─────────────────────────────────────┤ │ Production │ dk_19ffa8fc2a91f2cd9afb3bfecafef06d │ ├────────────┼─────────────────────────────────────┤ │ Staging │ dk_5g5dc7aa6197c1f292cd12ca28f39bb3 │ └────────────┴─────────────────────────────────────┘ ``` **Save these keys!** You'll need them in the next steps. ## Step 5: Update Your Dependencies Remove the old CodePush package: ```bash pnpm remove react-native-code-push ``` Then install Turbopush's version: ```bash pnpm add @turbopush/react-native-code-push ``` ## Step 6: Update iOS Configuration (Swift) ### 6.1 Update CocoaPods Run pod install to get the new Turbopush native module: ```bash cd ios && pod install && cd .. ``` ### 6.2 Check your AppDelegate.swift Open `ios/[YourProject]/AppDelegate.swift` and check if you have the following import: ```swift import React import CodePush // Same import - Turbopush is compatible! override func bundleURL() -> URL? { #if DEBUG RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") #else CodePush.bundleURL() // No change needed! #endif } ``` The import and method calls remain the same because Turbopush maintains full CodePush API compatibility. ### 6.3 Update Info.plist Open `ios/[YourProject]/Info.plist` and update the deployment key: ```xml CodePushDeploymentKey YOUR_NEW_TURBOPUSH_IOS_DEPLOYMENT_KEY ``` Replace `YOUR_NEW_TURBOPUSH_IOS_DEPLOYMENT_KEY` with the key from Step 4. ### 6.4 Verify iOS Deployment Target Make sure your `ios/Podfile` has the minimum deployment target: ```ruby platform :ios, '15.5' ``` ## Step 7: Update Android Configuration (Kotlin) ### 7.1 Update build.gradle In your `android/app/build.gradle`, update the CodePush gradle file reference at the end: ```kotlin apply from: "../../node_modules/react-native-code-push/android/codepush.gradle" // [!code --] apply from: "../../node_modules/@turbopush/react-native-code-push/android/codepush.gradle" // [!code ++] ``` ### 7.2 Update MainApplication.kt For React Native 0.76+, update your `android/app/src/main/java/.../MainApplication.kt`: ```kotlin import com.facebook.react.ReactHost import com.facebook.soloader.SoLoader import com.microsoft.codepush.react.CodePush // Same import! class MainApplication : Application(), ReactApplication { override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { override fun getJSBundleFile(): String { return CodePush.getJSBundleFile() // No change needed! } }; override fun onCreate() { super.onCreate() val deploymentKey = getString(R.string.CodePushDeploymentKey) // [!code ++] CodePush.getInstance(deploymentKey, this, BuildConfig.DEBUG) // [!code ++] loadReactNative(this) // [!code ++] } } ``` ### 7.3 Update strings.xml Update `android/app/src/main/res/values/strings.xml` with your new deployment key: ```xml YOUR_NEW_TURBOPUSH_ANDROID_DEPLOYMENT_KEY ``` Replace `YOUR_NEW_TURBOPUSH_ANDROID_DEPLOYMENT_KEY` with the key from Step 4. ## Step 8: Update JavaScript Code The JavaScript integration is nearly identical. Update your import statement: ```javascript import codePush from "react-native-code-push"; // [!code --] import codePush from "@turbopush/react-native-code-push"; // [!code ++] ``` Your `codePush()` wrapper and all options remain the same: ```javascript import codePush from "@turbopush/react-native-code-push"; function App() { // Your app code return ( // Your components ); } // All your existing options still work! export default codePush({ updateDialog: true, installMode: codePush.InstallMode.IMMEDIATE })(App); ``` ## Step 9: Build and Test Rebuild your app to apply the native changes: ```bash # For iOS cd ios && pod install && cd .. npx react-native run-ios --mode Release # For Android npx react-native run-android --mode release ``` ## Step 10: Release Your First Update with Turbopush Now let's verify everything works by pushing an update: #### iOS ```bash turbopush release-react myapp-ios ios -t "1.0.0" ``` #### Android ```bash turbopush release-react myapp-android android -t "1.0.0" ``` Make sure the version matches your app's current version in `Info.plist` (iOS) or `build.gradle` (Android). ### Release with Options The CLI options are similar to what you're used to: ```bash turbopush release-react myapp-ios ios --deploymentName Production --description "First Turbopush release" --mandatory --rollout 50 --targetBinaryVersion "1.0.0" ``` ## Migration Checklist Use this checklist to ensure you haven't missed anything: - [ ] Created Turbopush account - [ ] Installed `@turbopush/cli` - [ ] Authenticated with `turbopush login` - [ ] Created apps for iOS and Android - [ ] Replaced `react-native-code-push` with `@turbopush/react-native-code-push` - [ ] Updated `ios/Podfile` deployment target to 15.5+ - [ ] Ran `pod install` - [ ] Updated `Info.plist` with new iOS deployment key - [ ] Updated `android/app/build.gradle` with new gradle path - [ ] Updated `strings.xml` with new Android deployment key - [ ] Updated JavaScript imports - [ ] Built and tested app in Release mode - [ ] Successfully pushed first update ## Troubleshooting ### "No update available" after release - Double-check your deployment keys are correct - Verify the target binary version matches your app version exactly - Make sure you're targeting the right deployment (Staging vs Production) ### Build fails after migration - Run `cd ios && pod install && cd ..` to refresh CocoaPods - Clean your build: `cd android && ./gradlew clean && cd ..` - For iOS, clean in Xcode: Product → Clean Build Folder ### Updates not installing - Verify `codePush()` is wrapping your root component - Check your app is running in Release mode (Debug mode doesn't check for updates) - Review device logs for CodePush-related messages ## What About My Release History? Your release history from AppCenter won't be automatically migrated to Turbopush. However, this is usually not a problem because: 1. **Active users will get new updates** - Once you push your first Turbopush release, users will receive it 2. **No disruption** - Users won't experience any downtime during the transition 3. **Fresh start** - This is an opportunity to clean up old releases Simply release your current app state as your first Turbopush release, and you're back in business. ## Managing Releases via Dashboard While the CLI is powerful, you can also manage everything through the Turbopush web dashboard at [https://app.turbopush.org](https://app.turbopush.org?utm_source=blog&utm_campaign=migrating-from-appcenter-codepush-to-turbopush). The dashboard provides: - **Visual deployment history** - See all releases with timestamps and descriptions - **Real-time metrics** - Track downloads and installations - **Version distribution** - See which versions your users are running - **One-click rollbacks** - Instantly revert problematic updates - **Team collaboration** - Manage access and permissions ## Wrapping Up Migrating from AppCenter CodePush to Turbopush is straightforward because we've built Turbopush to be a drop-in replacement. The hardest part is updating a few configuration files—your core integration code stays virtually unchanged. With AppCenter's shutdown, you have a choice: spend days or weeks setting up and maintaining self-hosted infrastructure, or spend 30 minutes migrating to Turbopush and get back to building your app. **Ready to migrate?** Here are your next steps: - [Create your free account](https://app.turbopush.org/sign-up?utm_source=blog&utm_campaign=migrating-from-appcenter-codepush-to-turbopush) - [Read the full documentation](https://turbopush.org/docs?utm_source=blog&utm_campaign=migrating-from-appcenter-codepush-to-turbopush) - [Example Apps](https://turbopush.org/docs/examples/expo?utm_source=blog&utm_campaign=migrating-from-appcenter-codepush-to-turbopush) - [Website - Codepush alternative](https://turbopush.org/codepush-alternative?utm_source=blog&utm_campaign=migrating-from-appcenter-codepush-to-turbopush) Happy shipping! 🚀 # Ship Expo Updates (OTA) in Minutes — Turbopush Guide (/docs/blog/ship-expo-updates-in-minutes) If you've ever needed to fix a critical bug in your Expo app without waiting for App Store review, or wanted to roll out features instantly to your users, you're in the right place. Turbopush makes Over-The-Air (OTA) updates simple, fast, and reliable. **Looking for a cost-effective alternative to EAS Updates?** Turbopush offers the same power and flexibility at a fraction of the cost, with additional features like gradual rollouts, instant rollbacks, and detailed analytics. This guide will walk you through setting up Turbopush in your Expo project from scratch. Whether you're new to Over-The-Air updates or migrating from another solution, you'll be shipping updates in no time. ## What is Turbopush? Turbopush is an Over-The-Air (OTA) update platform that lets you push JavaScript bundle updates directly to your users' devices without going through app store reviews. Think of it as a deployment pipeline for your mobile app's code - similar to how you deploy web apps, but for React Native and Expo. **Perfect for:** - Bug fixes that can't wait for app store approval - A/B testing new features - Gradual rollouts to specific user segments - Quick iterations during development --- ## Want to See It in Action First? Before diving into the setup, you can test OTA updates right now with our pre-built Expo example app. No configuration needed - just scan and experience instant updates: **[Try the Live Demo →](https://turbopush.org/docs/examples/expo?utm_source=blog&utm_campaign=ship-expo-updates-in-minutes)** This working example lets you: - See how OTA updates download and install - Test mandatory vs. optional updates - Experience rollback functionality - Check update status in real-time Takes less than 2 minutes to try. Once you see it working, you'll know exactly what you're building! --- **Important: Turbopush does NOT work with Expo Go** Due to technical limitations, Turbopush requires native code modifications that aren't possible in the Expo Go app. You'll need to create a custom development build with `npx expo prebuild`, This is a one-time setup. Once you have a custom build installed on your device, OTA updates will work seamlessly. ## Step 1: Install the Turbopush CLI The Turbopush CLI is your command center for managing releases. Install it as a dev dependency in your project: ```bash pnpm add -D @turbopush/cli ``` Verify the installation worked: ```bash turbopush --version ``` You should see the CLI version printed out. If you do, you're good to go! ## Step 2: Create Your Turbopush Account Head over to [https://app.turbopush.org/sign-up](https://app.turbopush.org/sign-up) and create your free account. The process is straightforward - just your email and you're in. ## Step 3: Authenticate the CLI Now let's connect your local CLI to your Turbopush account: ```bash turbopush login ``` This command will: 1. Open your browser for authentication 2. Generate a secure access key 3. Prompt you to paste that key back in your terminal Once you see a success message, verify you're logged in: ```bash turbopush whoami ``` ### Set Your Default Organization If you're working with a team or have multiple organizations, set your default: ```bash turbopush org set ``` Not sure what your organization slug is? List them all: ```bash turbopush org list ``` ## Step 4: Register Your Apps Here's an important detail: **you need to create separate apps for iOS and Android**. This ensures each platform gets the right update packages. #### iOS ```bash turbopush app add MyApp-iOS --appSlug myapp-ios ``` #### Android ```bash turbopush app add MyApp-Android --appSlug myapp-android ``` The `--appSlug` parameter is optional but recommended. It's used to identify your app in the CLI and API. If you don't provide it, Turbopush will generate one from your app name. ### Save Your Deployment Keys After creating each app, you'll see output like this: ```bash ┌────────────┬─────────────────────────────────────┐ │ Name │ Deployment Key │ ├────────────┼─────────────────────────────────────┤ │ Production │ dk_19ffa8fc2a91f2cd9afb3bfecafef06d │ ├────────────┼─────────────────────────────────────┤ │ Staging │ dk_5g5dc7aa6197c1f292cd12ca28f39bb3 │ └────────────┴─────────────────────────────────────┘ ``` **Write these keys down!** You'll need them in the next step. Each app gets two deployment keys: - **Staging**: For testing updates before going live - **Production**: For your production users ## Step 5: Install the Turbopush SDK Now let's add Turbopush to your Expo project: ```bash expo install @turbopush/react-native-code-push @turbopush/turbopush-expo-plugin expo-build-properties ``` This installs three packages: - `@turbopush/react-native-code-push`: The core SDK - `@turbopush/turbopush-expo-plugin`: Expo configuration plugin - `expo-build-properties`: To set the minimum iOS deployment target (15.5 required) ## Step 6: Configure Your App Open your `app.json` (or `app.config.js`) and add the Turbopush configuration: ```json { "expo": { "plugins": [ [ "expo-build-properties", { "ios": { "deploymentTarget": "15.5" } } ], [ "@turbopush/turbopush-expo-plugin", { "android": { "CodePushDeploymentKey": "your-android-staging-key" }, "ios": { "CodePushDeploymentKey": "your-ios-staging-key" } } ] ] } } ``` Replace `your-android-staging-key` and `your-ios-staging-key` with the actual keys from Step 4. **Note:** The `expo-build-properties` plugin sets the iOS deployment target to 15.5, which is required by Turbopush. If you're already targeting iOS 15.5 or higher, you can skip this plugin. **Pro tip:** Use environment variables for deployment keys so you can easily switch between Staging and Production: ## Step 7: Run Prebuild Before building, you need to generate the native code with prebuild: ```bash expo prebuild ``` This command generates the `ios` and `android` folders with all the necessary native configurations from your plugins. ## Step 8: Integrate CodePush in Your Code The final code step is to wrap your app with the CodePush higher-order component. This is what checks for and downloads updates. Open your root component (usually `App.tsx` or `App.js`) and add: ```javascript import codePush from "@turbopush/react-native-code-push"; function App() { // Your app code here return ( // Your app components ); } export default codePush(App); ``` That's it for the code! The `codePush()` wrapper automatically handles checking for updates when your app starts. ### Want More Control? You can customize the update behavior by passing options directly to the `codePush()` wrapper: **Interactive updates** (show dialog and install immediately): ```javascript import codePush from "@turbopush/react-native-code-push"; function App() { // Your app code return ( // Your app components ); } export default codePush({ updateDialog: true, installMode: codePush.InstallMode.IMMEDIATE })(App); ``` **Custom update dialog**: ```javascript export default codePush({ updateDialog: { title: "Update Available", optionalUpdateMessage: "A new version is available. Install now?", optionalInstallButtonLabel: "Install", optionalIgnoreButtonLabel: "Later" }, installMode: codePush.InstallMode.IMMEDIATE })(App); ``` ## Step 9: Build and Run Your App Before you can push updates, you need to build and run your app at least once. This creates the native binary that will receive updates: #### iOS (simulator or device) ```bash expo run:ios --configuration Release ``` #### Android (emulator or device) ```bash expo run:android --variant release ``` **Important:** We're using `--configuration Release` (iOS) and `--variant release` (Android) to build in production mode. This ensures the app behaves exactly like it will in production with OTA updates enabled. This will compile the native code and install the app on your simulator/emulator or device. This is your baseline - all future OTA updates will be applied to this binary. ## Step 10: Release Your First Update Now for the exciting part - pushing your first update! Make a small change to your app (add some text, change a color, whatever you want to test). Then release it to Turbopush using the Expo-specific command: #### iOS (replace "1.0.0" with your app version) ```bash turbopush release-expo myapp-ios ios "1.0.0" ``` #### Android ```bash turbopush release-expo myapp-android android "1.0.0" ``` The CLI will: 1. Bundle your JavaScript code using Expo's bundler 2. Upload it to Turbopush 3. Make it available to your app immediately **Important:** The version number (`"1.0.0"`) must match exactly what's in your `app.json` under `expo.version`. ### Release with Options You can customize your release with additional parameters: ```bash turbopush release-expo myapp-ios ios "1.0.0" --deploymentName "Production" --description "Critical bug fixes" --mandatory --rollout 50 ``` **Parameters:** - `--deploymentName` or `-d`: Target deployment (default: "Staging") - `--description` or `--des`: Release notes - `--mandatory` or `-m`: Force users to install this update - `--rollout` or `-r`: Gradual rollout percentage (e.g., 25 = 25% of users) ## Step 11: Test Your Update Open your app on the test device. You should see one of these scenarios: 1. **Immediate update (mandatory)**: If your app was closed, it downloads and applies the update on restart 2. **Background update (non-mandatory)**: If your app was running, it downloads the update and applies it on the next restart Check your device logs to see the update process in action. You should see messages about checking for updates, downloading, and installing. ## Common Issues and Quick Fixes ### No updates available - Double-check your deployment keys are correct - Verify you're targeting the right deployment (Staging vs Production) - Make sure your app version matches the release target version ### Binary version mismatch Your release target version must match exactly what's in your `app.json`. If your app is `1.0.0`, release to `1.0.0`, not `1.0.1`. ### Updates not installing Make sure `codePush()` is wrapping your root component. The wrapper is what enables the update functionality. ## What's Next? Congratulations! You've successfully set up Turbopush and pushed your first OTA update. Here are some things to explore next: ### Multi-Deployment Strategy Use Staging for testing and Production for live users: #### Test on Staging First ```bash turbopush release-expo myapp-ios ios "1.0.0" -d Staging ``` #### Promote to Production ```bash turbopush promote myapp-ios Staging Production ``` ### Monitor Your Releases Check your deployment history and see how users are adopting updates: ```bash turbopush deployment history myapp-ios Production ``` ### Manage Updates via Dashboard While the CLI is powerful, you can also manage and monitor everything through the Turbopush web dashboard at [https://app.turbopush.org](https://app.turbopush.org). The dashboard gives you: - **Visual deployment history**: See all your releases with timestamps, descriptions, and rollout percentages - **Real-time metrics**: Track how many users have downloaded and installed each update - **Version distribution**: See which versions your users are running - **Easy rollbacks**: Rollback problematic updates with a single click - **Team collaboration**: Manage team access and permissions This is particularly useful when you need to quickly check the status of a deployment or share insights with non-technical team members who might not be comfortable with the CLI. ### Advanced Update Strategies - **Mandatory updates**: Force users to install critical fixes - **Gradual rollouts**: Release to 10% of users, then 50%, then 100% - **Target specific versions**: Send different updates to different app versions ## Why Turbopush? If you've used other OTA update solutions, here's what makes Turbopush different: **Cost-Effective Alternative to EAS Updates**: Get the same power and flexibility at a fraction of the cost. Turbopush offers all the OTA capabilities you need without the premium pricing of Expo's EAS Updates. **100% CodePush Compatible**: Drop-in replacement for AppCenter CodePush. No code changes needed - just update your deployment keys. **Universal Platform Support**: Works with bare React Native, Expo, and the New Architecture (Fabric + TurboModules). **Rich Analytics Dashboard**: Track install rates, version adoption, rollback events, and detailed user metrics in real-time. **Affordable Pricing**: Pay-as-you-grow with a generous free tier. Significantly more cost-effective than EAS Updates without compromising features. **Advanced Features**: Gradual rollouts, instant rollbacks, version targeting, mandatory updates, and delta updates for minimal bandwidth usage. **Modern Developer Experience**: Beautiful CLI, TypeScript SDK, comprehensive docs, and excellent support. ## Wrapping Up OTA updates are a game-changer for mobile development. What used to take days (or weeks) of app store review now takes seconds. Bug fixes, feature flags, A/B tests - all deployable instantly. Turbopush makes this power accessible with a simple CLI and minimal setup. You went from zero to shipping updates in about 10 steps, and now you have a deployment pipeline that rivals web development. Want to learn more? Check out: - [Turbopush vs Expo Updates: Comparison](https://turbopush.org/expo-updates-alternative?utm_source=blog&utm_campaign=ship-expo-updates-in-minutes) - [Website](https://turbopush.org?utm_source=blog&utm_campaign=ship-expo-updates-in-minutes) - [Documentation](https://turbopush.org/docs?utm_source=blog&utm_campaign=ship-expo-updates-in-minutes) - [Getting Started](https://turbopush.org/docs?utm_source=blog&utm_campaign=ship-expo-updates-in-minutes) - [Example Apps](https://turbopush.org/docs/examples/expo?utm_source=blog&utm_campaign=ship-expo-updates-in-minutes) Happy shipping! 🚀 # Web Deploys vs React Native: Mobile Is Stuck in 2010 (/docs/blog/web-deploys-vs-mobile-releases) Shipping a one-line fix to a mobile app still takes days. The same fix on web takes minutes: a developer merges a pull request and it's live before their coffee gets cold. On mobile, the same merge means building a binary, bumping a version, submitting for review, waiting, releasing, and hoping users actually update. Same bug. Same fix. Two completely different decades. ![Timeline comparison: web deploys and OTA updates reach users in minutes, while an app store release spans build, review, and weeks of user adoption](/docs/merge-to-users-timeline.svg) Here's why that gap happened, what it actually costs teams, and how to close it. ## How the Web Got Fast It's easy to forget that web deploys used to be scary too. In the 2000s, "deploying" often meant FTPing files to a server on a Friday and praying. Releases were events: scheduled, announced, and staffed late into the night. Then, in the span of a few years, everything changed: - **Continuous integration became table stakes.** Every commit built and tested automatically. - **Continuous deployment stopped being radical.** Companies like Flickr and Etsy made "deploy dozens of times a day" a normal engineering practice, not a stunt. - **Rollbacks became a button.** If a deploy went bad, you reverted in minutes. Bad deploys went from disasters to non-events. - **Feature flags decoupled deploy from release.** Code could ship dark and turn on gradually. What speed actually bought them wasn't just velocity. It was a different relationship with risk: when shipping is cheap and reversible, you ship small changes often, each one easy to reason about. When shipping is expensive, you batch changes into big risky releases. Hold on to those two words, **cheap** and **reversible**. Mobile got neither. ## Meanwhile, on Mobile ### The same fix, side by side Put the two workflows next to each other and the gap is hard to unsee: | | Web in 2026 | Mobile in 2026 | |---|---|---| | Time from merge to users | Minutes | Days to weeks | | Who approves the release | Your team | An external reviewer | | Rollback | One click, instant | A whole new release, plus review | | Users on the latest version | Everyone, on next page load | Whoever chose to update | | Deploy frequency | Daily or more | Weekly to monthly release trains | | A bad Friday deploy | Revert and go home | A very bad weekend | That right column isn't describing 2026. It's describing how the web worked in 2010, with an extra gatekeeper on top. And to be fair to Apple and Google: the review queue isn't even the slow part anymore. Reviews usually clear in about a day. The real gap between "merge" and "users" is adoption, waiting for people to actually install the new version. That part can take weeks, and there's no expedited request for it. ## Why Mobile Got Stuck None of this is because mobile engineers are behind the times. The constraints are structural. ### Distribution is owned by gatekeepers On the web you own the server, so you own the release. On mobile, Apple and Google sit between you and your users. Their review process exists for real reasons: malware, scams, platform quality. But it means every release, even a typo fix, pays the same toll. ### The artifact is a binary on someone else's device A website lives in one place, and you update that place. An app is copied onto millions of devices you don't control, running versions you can't retire. Weeks after a release, a meaningful share of your users are still on the old version, bug included. ### Rollback barely exists Neither store lets you truly roll back. Your best option is submitting yet another release, through the same review queue, while the broken version keeps hurting users. The one mechanism that made web deploys safe is the one mobile never got. ### So teams adapted around the constraint Sound familiar? Every one of these is a rational response to expensive shipping:
**Release trains** Ship every two weeks, because shipping daily is impossible.
**Code freezes** Stop the world so the release can be stabilized.
**One-shot QA cycles** Test everything up front, because there's no cheap fix later.
**Hotfix war rooms** Escalation paths, expedited review requests, crossed fingers.
All reasonable. All symptoms of the same disease: shipping is expensive, so everything gets built around shipping rarely. ## What Getting Stuck Actually Costs The cost isn't just slow hotfixes, though those are painful enough. It's what expensive releases do to how teams work. ![The fear spiral: shipping is expensive, so changes get batched, so releases get bigger, so debugging gets harder, so there is more fear and more process, which makes shipping even more expensive](/docs/fear-spiral.svg) Once the spiral is spinning, it touches everything: - **Experiments become luxuries.** Every iteration costs a review cycle, so teams stop trying small things. Small things cost as much to ship as big ones. - **Crash windows are measured in days.** On the web, the clock to resolution runs in minutes. On mobile: expedited review if you're lucky, and then adoption lag on top. Roughly half of your users are still on the old version a week after the fix ships, crash included. - **Roadmaps get defensive.** When a mistake takes a week to undo, "let's not risk it" wins more arguments than it should. > Adoption lag isn't something you can out-engineer with better update prompts. Even Apple, with near-total control of the iOS update experience, needed around 150 days to get iOS 26 onto 74% of recent devices. Your app has far less leverage than the operating system does. > And while you wait on a fix, your app's rating takes the hit. Ratings don't roll back either. ## The Escape Hatch: Shipping Like the Web Again Here's the thing that changed the math: for React Native and Expo apps, most of what you ship day to day isn't native code. It's JavaScript. Your components, your logic, your styles, your fixes. And JavaScript doesn't need an app store to travel. > Is this allowed? Yes. Both Apple's guidelines and Google's policies permit updating JavaScript served by frameworks like React Native, as long as updates don't change the app's core purpose. This is the same mechanism Expo Updates and the now-discontinued CodePush built on. ### How OTA updates close the gap That's what [Over-The-Air updates](/docs/blog/what-are-ota-updates-and-why-they-matter) are: your app checks a server for a new JS bundle, downloads it in the background, and swaps it in. - **No new binary** to build and sign - **No review queue** for JavaScript changes - **No waiting on users** to visit the store: the update applies on next launch Native code changes still go through the normal release process (and should). But the everyday changes, the ones that used to wait for the train, ship like a web deploy. ![One month of shipping compared: two large risky app store releases versus around forty small OTA updates, where a bad update is rolled back in minutes](/docs/release-train-vs-continuous.svg) ### The table, revisited | | Store release | OTA update | |---|---|---| | Time from merge to users | Days to weeks | Minutes | | Review | Required | Not required for JS changes | | Rollback | New release, new review | Instant, one command | | Users on the latest version | Whoever updates | Everyone, on next launch | The web needed cheap, reversible deploys to escape 2010. OTA gives mobile exactly those two properties. ## Where Turbopush Fits Here's what that same one-line fix looks like with Turbopush: you merge the PR, CI builds the JS bundle, and Turbopush serves it to your app. On their next launch, users download the update in the background and get the fix. No binary, no review, no waiting. You watch adoption climb in real time from the dashboard. If something goes wrong, you hit rollback and every user reverts to the previous version on their next launch. One click, no review queue. Turbopush is an Over-The-Air update platform built for React Native, supporting both bare React Native and Expo. There are no storage or bandwidth charges on any plan, so the cost of shipping often stays flat. (We did a full [pricing breakdown against Expo Updates](/docs/blog/what-are-ota-updates-and-why-they-matter#pricing-comparison-turbopush-vs-expo-updates) if you want the numbers.) Want to see the loop in action? [Try the live demo](/docs/examples/expo?utm_source=blog&utm_campaign=web-deploys-vs-mobile-releases), it takes about two minutes. ## Conclusion The web didn't get fast because web developers were smarter. It got fast because deploys became cheap and reversible, and everything else followed: smaller changes, less fear, faster iteration. Mobile has been stuck on the other side of that line, not for technical reasons but for distribution ones. [OTA updates](/docs/blog/what-are-ota-updates-and-why-they-matter) move the everyday work of shipping back under your control, where it belongs. Your users don't care how the fix arrived. They care that it arrived today instead of next week. [Get started with Turbopush](/docs?utm_source=blog&utm_campaign=web-deploys-vs-mobile-releases) and ship your next fix in minutes. # What Are Over-The-Air Updates and Why They Matter for React Native (/docs/blog/what-are-ota-updates-and-why-they-matter) Imagine shipping a critical bug fix and having it reach users in minutes, not days. That's the power of Over-The-Air (OTA) updates, and it's changing how mobile developers ship software. Here's what you'll get: what OTA updates are, how they work in React Native, and a real pricing comparison between Turbopush and Expo Updates. ![Traditional app store release process vs OTA release process with Turbopush](/docs/ota-architecture.svg) ## What Exactly Are OTA Updates? OTA updates let you push new code straight to users' devices, no app store submission required. No new binary, no review, no waiting on users to update. You deploy, and it's live. Think of it like deploying a website. Push a change, and users see it on their next visit. OTA updates bring that same workflow to mobile. | Can Update with OTA ✅ | Cannot Update with OTA ❌ | |---|---| | JavaScript code and logic | Native code (Swift, Kotlin, Objective-C, Java) | | React components and screens | Native dependencies and libraries | | Styles and layouts | App permissions and capabilities | | Images and assets bundled with your code | App Store metadata (name, icon, screenshots) | ## How OTA Updates Work in React Native React Native apps are really made of two parts: the **native shell** (compiled code on the device) and the **JavaScript bundle** (your React components, logic, and UI). That split is what makes OTA updates possible. Your JS code gets bundled into a single file that the native shell loads at runtime. So what happens when you ship an update?
This is why React Native suits OTA updates so well. A fully native app means recompiling and resubmitting for every change. Here, you just push JavaScript, and it reaches your users without them lifting a finger. ## Why OTA Updates Matter ### 1. Speed to Market Ever waited on an App Store review while a bug burns through your users' trust? Reviews take anywhere from 24 hours to several days, and for a critical fix, that delay costs you users and revenue. OTA updates skip the wait: deploy a fix in seconds, not days. ### 2. Reduced User Friction Not everyone updates their apps. Some disable auto-updates, some ignore the notification, most just forget. OTA updates sidestep all that: users get the latest version either way. > Remember that ~1 week head start from earlier? This is where it comes from: every one of these reasons ties back to skipping the app store's review queue. ### 3. A/B Testing and Feature Flags Want to try a new feature on 10% of your users before betting the whole app on it? Most OTA platforms give you rollout controls for exactly that: ship gradually, watch what happens, roll back instantly if it goes sideways. ### 4. Emergency Response When production breaks, every minute matters. You don't want to be refreshing App Store Connect hoping for approval. With OTA, you just ship the fix. ## What is Turbopush? Turbopush is an Over-The-Air update platform built for React Native. Bare React Native or Expo, it drops into your existing workflow without a fight. **Key features:** - **Universal React Native support**: Works with bare React Native, Expo, and the New Architecture (Fabric + TurboModules) - **Gradual rollouts**: Release to 10%, 50%, or any percentage of users before going full - **Instant rollbacks**: Revert problematic updates with a single command - **Rich analytics**: Track install rates, version adoption, and user metrics in real-time ## Pricing Comparison: Turbopush vs Expo Updates Picking an OTA platform comes down to price eventually. Let's put Turbopush and Expo Updates (EAS Update) side by side across a few app sizes. ### Real-World Scenarios Here's how the two platforms compare at different stages of growth: Turbopush is up to 94% cheaper than Expo Updates. ![Monthly cost comparison chart: Turbopush vs Expo Updates across 10K, 20K, 100K, and 300K user scenarios, with the plan required at each stage](/docs/pricing-comparison-chart.svg) > Expo's overage fees kick in the moment you cross a plan's MAU limit. Turbopush just needs one plan whose allowance covers you, no per-user math required. ### Understanding the Pricing Models Both platforms charge by **Monthly Active Users (MAUs)**: unique users who receive at least one update per billing period, no matter how many times they update. The difference is how much MAU you get per dollar, and what's included on top. Here's the full breakdown: ### No Hidden Costs: Storage & Bandwidth Included One overlooked part of OTA pricing is **storage and bandwidth**. Most OTA platforms, including Expo Updates, charge extra for both. **Spoiler: Turbopush doesn't.** - **Storage:** $0.05 per GB after your plan's limit (20 GB on Starter, 1 TB on Production) - **Bandwidth:** $0.10 per GB after your plan's limit (10 GB on Starter, 100 GB on Production) These costs add up fast, especially with bundled assets or frequent updates to a large user base. A single 5 MB update to 100,000 users burns ~500 GB of bandwidth. That's $40 alone, on top of your base plan. **Turbopush doesn't charge for storage or bandwidth on any plan.** No surprise charges, no metering bundle sizes, no counting assets. Your monthly cost is exactly what you see, nothing more. ## Getting Started with Turbopush Ready to try it yourself? Setting up Turbopush takes less than 30 minutes. Our step-by-step guide walks you through creating your account, installing the SDK, configuring your app, and shipping your first update. We've got dedicated guides whether you're on bare React Native or Expo. **[Read the Getting Started Guide →](https://turbopush.org/docs?utm_source=blog&utm_campaign=what-are-ota-updates-and-why-they-matter)** ## Conclusion OTA updates change how you ship mobile apps. Decouple your JavaScript from app store releases, and you get the speed and flexibility web developers have had for years. Whether you're squashing a bug, testing a feature, or just iterating faster, OTA updates remove the friction. With pricing this generous, there's not much reason to wait. The question isn't whether you should use OTA updates. It's how fast you want to get started. --- **Ready to ship faster?** - [Turbopush Website](https://turbopush.org?utm_source=blog&utm_campaign=what-are-ota-updates-and-why-they-matter) - [Documentation](https://turbopush.org/docs?utm_source=blog&utm_campaign=what-are-ota-updates-and-why-they-matter) - [Getting Started Guide](https://turbopush.org/docs?utm_source=blog&utm_campaign=what-are-ota-updates-and-why-they-matter) - [Try the Live Demo](https://turbopush.org/docs/examples/expo?utm_source=blog&utm_campaign=what-are-ota-updates-and-why-they-matter) - [Create Your Free Account](https://app.turbopush.org/sign-up?utm_source=blog&utm_campaign=what-are-ota-updates-and-why-they-matter) Happy shipping! 🚀 # What is CodePush? A Complete Guide to Over-the-Air Updates for React Native (/docs/blog/what-is-codepush) Ship a React Native app and you learn the drill fast. A bug slips through, your fix takes five minutes, and then you wait days for App Store review while users suffer. React Native CodePush fixes this: ship JavaScript updates straight to devices, no review required. Fix goes live in minutes. Microsoft shut down the original AppCenter CodePush in March 2025, but the technology and the name both stuck around. Here is what CodePush is, how it works, why it shut down, and what to use instead.
**TL;DR** - **CodePush is an over-the-air update service** that lets you push JavaScript and asset updates to React Native apps without App Store review. - **Microsoft shut down AppCenter CodePush on March 31, 2025.** The name is still the standard term developers use when searching for OTA update solutions. - **Turbopush is a 100% API-compatible CodePush alternative** that costs up to 94% less than EAS Updates. Migration takes under 30 minutes.
## What is CodePush? CodePush is an over-the-air (OTA) update service for React Native and Cordova apps. It lets developers deploy updates to JavaScript, images, and assets without submitting a new binary to the App Store or Google Play. The name comes from Microsoft's AppCenter CodePush, the first widely adopted OTA platform for mobile. Microsoft bought it in 2015 and ran it until March 31, 2025. Even after the shutdown, "CodePush" stuck as the term developers search for when they need OTA updates. ## How CodePush Works React Native apps are made of two layers: a native shell (Swift, Kotlin, Objective-C, Java) and a JavaScript bundle (your components, logic, styles). That separation is what makes CodePush possible. If you are new to the concept, read our [guide to OTA updates](/docs/blog/what-are-ota-updates-and-why-they-matter). Your JavaScript gets compiled into a single file that the native shell loads at runtime. CodePush swaps that file out over the air. ![React Native CodePush OTA architecture: traditional app store release vs OTA update process](/docs/codepush-ota-architecture.svg)
| What You Can Update with CodePush | What You Cannot Update | |---|---| | JavaScript logic and business rules | Native code (Swift, Kotlin, Java, Objective-C) | | React components and screens | Native library dependencies | | CSS styles and layout | App permissions and entitlements | | Images and bundled assets | App Store listing metadata | | Navigation and routing | SDK version changes | ## The History: Microsoft, AppCenter, and the Shutdown Microsoft acquired CodePush in 2015 and made it the default OTA update service for React Native inside Visual Studio AppCenter. In March 2024, Microsoft announced AppCenter would retire on March 31, 2025. AppCenter never became core to Microsoft's strategy: GitHub Actions and Azure DevOps already handled CI/CD, and CodePush got caught in the crossfire. The shutdown left thousands of teams looking for a CodePush alternative. CodePush had powered updates for apps with millions of users. Options boiled down to three: find a replacement, self-host, or abandon OTA updates. ## Why React Native CodePush Matters ### Shipping Speed App Store review takes 24 hours to several days. Google Play is faster but still not instant. CodePush-style updates skip the queue. A bug fix goes from your terminal to production in minutes. ### Users Never Fall Behind Most users do not update their apps. Auto-updates get disabled, notifications get ignored, old phones cannot run the latest binary. CodePush keeps your JavaScript current regardless of which binary is installed. ### Gradual Rollouts Ship a new feature to 10% of your users first. Watch crash rates. If stable, expand. If something breaks, roll back in one click. CodePush gives you deployment controls that the app stores do not. ### Emergency Fixes When production is on fire, App Store review is not an option. CodePush lets you ship a fix immediately. ## CodePush Alternatives After the Shutdown With AppCenter CodePush gone, several platforms stepped in. Here is what replaced it. ### Turbopush Turbopush was built as a drop-in replacement for AppCenter CodePush. Your imports stay the same. Your deployment keys point to Turbopush servers instead of Microsoft's. The SDK is 100% API-compatible with `react-native-code-push`. Turbopush costs up to 94% less than Expo EAS Updates and up to 60% less than other CodePush alternatives. Pricing starts at $15 per month for 30,000 monthly active users. A free tier covers 2,000 MAUs with 20 releases per month. Storage and bandwidth are included on every plan. No overage surprises. Turbopush supports bare React Native, Expo, and the New Architecture (Fabric and TurboModules). Migrating from AppCenter CodePush takes under 30 minutes. ### Expo EAS Updates Expo's built-in OTA service is the default for Expo projects. The integration is tight, especially if you already use EAS Build. The catch is pricing. The Production plan starts at $199 per month for 50,000 MAUs, with per-user overage fees once you cross the limit. Storage and bandwidth cost extra too. See our [step-by-step guide to shipping Expo OTA updates](/docs/blog/ship-expo-updates-in-minutes). ### Self-Hosting Microsoft open-sourced a reference server called [code-push-server](https://github.com/microsoft/code-push-server). Self-hosting gives you full control and no monthly SaaS bill. But you own everything: servers, databases, CDN, SSL certificates, monitoring, backups, and scaling. For most teams, the engineering time alone costs more than a managed service. > Self-hosting seems free until you factor in the DevOps hours. A single engineer spending two days per month on maintenance already exceeds the cost of Turbopush's Growth plan. ## Getting Started Ready to get React Native CodePush running? The SDK is a drop-in for `react-native-code-push`, so existing integrations work with minimal changes. If you are new, the getting started guide walks through your first release. **[Read the Getting Started Guide →](https://turbopush.org/docs?utm_source=blog&utm_campaign=what-is-codepush)** ## Conclusion CodePush turned mobile releases from a multi-day ritual into something that happens in minutes. Microsoft shut down the original service, but React Native CodePush is here to stay. Whether you need a CodePush replacement or you are exploring OTA updates for the first time, the options today are better than ever. The question is not whether your app needs OTA updates. It is which platform you pick. --- **Ready to ship faster?** - [Turbopush Website](https://turbopush.org/codepush-alternative?utm_source=blog&utm_campaign=what-is-codepush) - [Documentation](https://turbopush.org/docs?utm_source=blog&utm_campaign=what-is-codepush) - [Migration Guide (AppCenter → Turbopush)](https://turbopush.org/docs/blog/migrating-from-appcenter-codepush-to-turbopush?utm_source=blog&utm_campaign=what-is-codepush) - [Try the Live Demo](https://turbopush.org/docs/examples/expo?utm_source=blog&utm_campaign=what-is-codepush) - [Create Your Free Account](https://app.turbopush.org/sign-up?utm_source=blog&utm_campaign=what-is-codepush) # Expo Example (/docs/examples/expo) This example demonstrates a complete Expo application with Turbopush integration, allowing you to test over-the-air updates in minutes. ## What you'll see here - **QR Code Ready to Use** - We have a pre-built APK ready for you to test immediately without building anything. - Publish an update to your app and test it over-the-air. - See how fast you can release an update to your users. - Troubleshooting tips if you encounter any issues. ## Quick Start ### 1. Clone the Repository ```bash git clone https://github.com/turbopushdev/example-expo-turbopush.git ``` ```bash cd example-expo-turbopush ``` ### 2. Install Dependencies ```bash pnpm install ``` ### 3. Log in to Turbopush > if you already did this before, you can skip this step. ```bash turbopush login ``` This will open your browser to authenticate. Once authenticated, you can close the browser and continue with the next steps. ### 4. Create an app ```bash turbopush app add ``` Copy the Staging Deployment Key and keep it handy, you'll need it in the next steps. ### 5. Install App **Qr Code** **QR Code Ready to Use** Want to test immediately without building? We have a pre-built APK ready for you. Simply scan this QR Code: **Build App** > Don't want to build the app? We have a pre-built APK ready for you. Change the tab above to "QR Code" and simply scan and install the app. ```bash cp .env.example .env ``` Open the .env file and paste the Staging Deployment Key you copied in the previous step. ### 6. Configure Your Deployment Key Paste the Staging Deployment Key you copied in the previous step to generate a QR code: ### 7. Test an Over-the-Air Update Now let's publish an update to test the OTA functionality: 1. Make a change in the **src/app/playground.tsx** file, in the config object at the top of the file. 2. Release the update using Turbopush CLI: ```bash turbopush release-expo android 1.0.0 -d Staging --mandatory ``` 3. Click in the "sync" button in the initial screen. 4. Watch the update download and install automatically! 5. Click in the "Playground" button in the initial screen to see the change you made. 6. That's it! You've successfully published an update to your app and tested it over-the-air. ## Next Steps - [Learn about releasing updates](/docs/cli/releasing-updates/expo) ## Troubleshooting ### Updates not appearing? 1. Check that the update's target binary version matches your app version 2. Verify your deployment keys are correct 3. Check the logs in screen to see if there are any errors. ### Still not working? [open an issue](https://github.com/turbopushdev/react-native-code-push/issues). # Android Setup (/docs/setup/android) In order to integrate Turbopush into your Android project, please perform the following steps: 1. Add this line at the end of the file: ```kotlin title="android/app/build.gradle" apply from: "../../node_modules/@turbopush/react-native-code-push/android/codepush.gradle" ``` 2. Update the `MainApplication` file to use CodePush with the following changes: **Kotlin Rn82** ```kotlin title="android/app/src/main/java/com//MainApplication.kt" import com.facebook.react.ReactHost import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.microsoft.codepush.react.CodePush // [!code ++] class MainApplication : Application(), ReactApplication { override val reactHost: ReactHost by lazy { getDefaultReactHost( context = applicationContext, packageList = PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: // add(MyReactNativePackage()) }, jsBundleFilePath = CodePush.getJSBundleFile(), // [!code ++] ) } override fun onCreate() { super.onCreate() val deploymentKey = getString(R.string.CodePushDeploymentKey) // [!code ++] CodePush.getInstance(deploymentKey, this, BuildConfig.DEBUG) // [!code ++] loadReactNative(this) } } ``` **Kotlin** ```kotlin title="android/app/src/main/java/com//MainApplication.kt" import com.facebook.react.ReactHost import com.facebook.soloader.SoLoader import com.microsoft.codepush.react.CodePush // [!code ++] class MainApplication : Application(), ReactApplication { override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { override fun getJSBundleFile(): String { // [!code ++] return CodePush.getJSBundleFile() // [!code ++] } // [!code ++] }; override fun onCreate() { super.onCreate() val deploymentKey = getString(R.string.CodePushDeploymentKey) // [!code ++] CodePush.getInstance(deploymentKey, this, BuildConfig.DEBUG) // [!code ++] loadReactNative(this) } } ``` 3. Add the CodePushDeploymentKey key (generated in the create app step): ```xml title="android/app/src/main/res/values/strings.xml" YOUR_ANDROID_DEPLOYMENT_KEY ``` > `turbopush deployment list -a -k` to get the deployment key for your app. > If you need to dynamically use a different deployment, you can also override your deployment key in JS code using [options](./api-js#codepushoptions). ### Code Signing setup (optional) Starting with CLI version **2.1.0** you can self sign bundles during release and verify its signature before installation of update. For more info about Code Signing please refer to [relevant documentation section](../cli/code-signing). In order to use Public Key for Code Signing you need to do following steps: Add `CodePushPublicKey` string item to `/path_to_your_app/android/app/src/main/res/values/strings.xml`. It may looks like this: ```xml my_app -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtPSR9lkGzZ4FR0lxF+ZA P6jJ8+Xi5L601BPN4QESoRVSrJM08roOCVrs4qoYqYJy3Of2cQWvNBEh8ti3FhHu tiuLFpNdfzM4DjAw0Ti5hOTfTixqVBXTJPYpSjDh7K6tUvp9MV0l5q/Ps3se1vud M1/X6g54lIX/QoEXTdMgR+SKXvlUIC13T7GkDHT6Z4RlwxkWkOmf2tGguRcEBL6j ww7w/3g0kWILz7nNPtXyDhIB9WLH7MKSJWdVCZm+cAqabUfpCFo7sHiyHLnUxcVY OTw3sz9ceaci7z2r8SZdsfjyjiDJrq69eWtvKVUpredy9HtyALtNuLjDITahdh8A zwIDAQAB -----END PUBLIC KEY----- ``` # Javascript Reference (API) (/docs/setup/api-js) The CodePush plugin has two components: 1. **JavaScript module** - Interact with the service at runtime (check updates, inspect metadata) 2. **Native API** (Objective-C/Java) - Bootstrap the app with the correct JS bundle location ## Top-level Methods When you import `react-native-code-push`, you get these methods: | Method | Description | |--------|-------------| | [`allowRestart`](#codepushallowrestart) | Re-allows programmatic restarts after `disallowRestart` | | [`checkForUpdate`](#codepushcheckforupdate) | Check if an update is available | | [`disallowRestart`](#codepushdisallowrestart) | Temporarily block restarts during critical flows | | [`getUpdateMetadata`](#codepushgetupdatemetadata) | Get metadata about installed updates | | [`notifyAppReady`](#codepushnotifyappready) | Mark update as successful (prevents rollback) | | [`restartApp`](#codepushrestartapp) | Immediately restart the app | | [`sync`](#codepushsync) | Check, download, and install updates in one call | | [`clearUpdates`](#clearupdates) | Clear all downloaded updates.
Note: we don't recommend using this method in scenarios other than OTA debugging/testing; CodePush will call it automatically when appropriate. Using it in other cases could lead to unpredictable behavior. | ### codePush ```javascript // Wrapper function codePush(rootComponent: React.Component): React.Component; codePush(options: CodePushOptions)(rootComponent: React.Component): React.Component; ``` Wraps a React component to automatically sync updates on mount. Internally calls [`sync`](#codepushsync) in `componentDidMount`. #### Usage Examples **1. Silent sync on app start** *(default behavior)* Updates download silently and apply on next restart. ```javascript const MyApp = () => ; export default codePush(MyApp); ``` **2. Silent sync on every resume** ```javascript const MyApp = () => ; export default codePush({ checkFrequency: codePush.CheckFrequency.ON_APP_RESUME, installMode: codePush.InstallMode.ON_NEXT_RESUME })(MyApp); ``` **3. Interactive with prompt** Prompts user before downloading. Mandatory updates show notification but can't be ignored. ```javascript const MyApp = () => ; export default codePush({ updateDialog: true, installMode: codePush.InstallMode.IMMEDIATE })(MyApp); ``` **4. With progress tracking** ```javascript const MyApp = () => { const codePushStatusDidChange = (status) => { const messages = { [codePush.SyncStatus.CHECKING_FOR_UPDATE]: "Checking for updates.", [codePush.SyncStatus.DOWNLOADING_PACKAGE]: "Downloading package.", [codePush.SyncStatus.INSTALLING_UPDATE]: "Installing update.", [codePush.SyncStatus.UP_TO_DATE]: "Up-to-date.", [codePush.SyncStatus.UPDATE_INSTALLED]: "Update installed.", }; console.log(messages[status] || "Unknown status"); }; const codePushDownloadDidProgress = ({ receivedBytes, totalBytes }) => { console.log(`${receivedBytes} of ${totalBytes} received.`); }; return ; }; // Attach hooks as static properties MyApp.codePushStatusDidChange = codePushStatusDidChange; MyApp.codePushDownloadDidProgress = codePushDownloadDidProgress; export default codePush(MyApp); ``` ### CodePushOptions | Option | Type | Default | Description | |--------|------|---------|-------------| | `checkFrequency` | CheckFrequency | `ON_APP_START` | When to check for updates | | `deploymentKey` | String | From config | Override deployment key at runtime | | `installMode` | InstallMode | `ON_NEXT_RESTART` | When to install optional updates | | `mandatoryInstallMode` | InstallMode | `IMMEDIATE` | When to install mandatory updates | | `minimumBackgroundDuration` | Number | `0` | Seconds in background before restart (for `ON_NEXT_RESUME`/`ON_NEXT_SUSPEND`) | | `updateDialog` | UpdateDialogOptions | `null` | Configure update prompt dialog | | `rollbackRetryOptions` | RollbackRetryOptions | `null` | Configure rollback retry behavior | #### UpdateDialogOptions | Option | Default | Description | |--------|---------|-------------| | `appendReleaseDescription` | `false` | Append release description to notification | | `descriptionPrefix` | `" Description: "` | Prefix for release description | | `mandatoryContinueButtonLabel` | `"Continue"` | Button text for mandatory updates | | `mandatoryUpdateMessage` | `"An update is available that must be installed."` | Message for mandatory updates | | `optionalIgnoreButtonLabel` | `"Ignore"` | Ignore button text | | `optionalInstallButtonLabel` | `"Install"` | Install button text | | `optionalUpdateMessage` | `"An update is available. Would you like to install it?"` | Message for optional updates | | `title` | `"Update available"` | Dialog title | #### RollbackRetryOptions The rollback retry mechanism allows the application to attempt to reinstall an update that was previously rolled back (with the restrictions specified in the options). | Option | Default | Description | |--------|---------|-------------| | `delayInHours` | `24` | Minimum hours before retrying rolled-back update | | `maxRetryAttempts` | `1` | Maximum retry attempts (min: 1) | #### Event Hooks **codePushStatusDidChange** Called when sync moves between stages. Receives a [`SyncStatus`](#syncstatus) code. **codePushDownloadDidProgress** Called periodically during download with: - `totalBytes` - Total bytes to download - `receivedBytes` - Bytes downloaded so far --- ### codePush.allowRestart ```javascript codePush.allowRestart(): void; ``` Re-allows restarts after `disallowRestart`. If an update was pending restart, it triggers immediately. **Won't trigger restart if:** - No updates installed since `disallowRestart` - Pending update uses `ON_NEXT_RESTART` mode - Pending update uses `ON_NEXT_RESUME` but app hasn't backgrounded yet - No `restartApp` calls since `disallowRestart` --- ### codePush.checkForUpdate ```javascript codePush.checkForUpdate( deploymentKey?: string, handleBinaryVersionMismatchCallback?: (update: RemotePackage) => void ): Promise; ``` Checks if an update is available. **Returns `null` when:** - No releases in deployment - Latest release targets different binary version - App already has latest release - Latest release is disabled - Device not in active rollout percentage **Returns [`RemotePackage`](#remotepackage)** when update is available. ```javascript const update = await codePush.checkForUpdate(); if (!update) { console.log("The app is up to date!"); } else { console.log("An update is available! Should we download it?"); } ``` The optional `handleBinaryVersionMismatchCallback` notifies when binary updates are available (e.g., installed 1.0.1 but 1.0.2 exists). > Be cautious using Alerts in this callback for iOS apps due to App Store guidelines about forced actions. --- ### codePush.disallowRestart ```javascript codePush.disallowRestart(): void; ``` Temporarily blocks restarts from: 1. Updates with `InstallMode.IMMEDIATE` 2. Updates with `ON_NEXT_RESUME` when app resumes 3. Direct `restartApp` calls Useful for protecting critical flows like onboarding. ```javascript const OnboardingProcess = () => { useEffect(() => { // Block restarts during onboarding codePush.disallowRestart(); return () => { // Re-allow on unmount (triggers pending restart if any) codePush.allowRestart(); }; }, []); return ; }; ``` --- ### codePush.getUpdateMetadata ```javascript codePush.getUpdateMetadata( updateState: UpdateState = UpdateState.RUNNING ): Promise; ``` Gets metadata for installed updates matching the specified state. **Returns `null` when:** - No CodePush updates installed yet - Binary update cleared old CodePush updates - `RUNNING` requested but no CodePush update is running - `PENDING` requested but no pending updates ```javascript // Get running update info const update = await codePush.getUpdateMetadata(); if (update) { analytics.track({ CodePushRelease: update.label }); } // Check for pending updates const pending = await codePush.getUpdateMetadata(codePush.UpdateState.PENDING); if (pending) { // Maybe force restart? } ``` --- ### codePush.notifyAppReady ```javascript codePush.notifyAppReady(): Promise; ``` Marks the current update as successful, preventing automatic rollback on next restart. > If using `sync`, this is called automatically. Only needed for manual update flows. --- ### codePush.restartApp ```javascript codePush.restartApp(onlyIfUpdateIsPending?: boolean): void; ``` Immediately restarts the app. If `onlyIfUpdateIsPending` is `true`, only restarts if there's a pending update. Useful when using `ON_NEXT_RESTART`/`ON_NEXT_RESUME` modes and you have a good moment to apply updates (e.g., user returns to home screen). --- ### codePush.sync ```javascript codePush.sync( options?: SyncOptions, syncStatusChangeCallback?: (status: SyncStatus) => void, downloadProgressCallback?: (progress: DownloadProgress) => void, handleBinaryVersionMismatchCallback?: (update: RemotePackage) => void ): Promise; ``` All-in-one method: checks, downloads, and installs updates. **Two modes:** 1. **Silent** *(default)* - Downloads silently, applies on next restart 2. **Active** - Prompts user, applies immediately ```javascript // Silent update codePush.sync(); // Active with prompt codePush.sync({ updateDialog: true, installMode: codePush.InstallMode.IMMEDIATE }); ``` #### SyncOptions Same as [CodePushOptions](#codepushoptions) except `checkFrequency`. ```javascript // Different deployment key codePush.sync({ deploymentKey: "KEY" }); // Install on resume after 5 min in background codePush.sync({ installMode: codePush.InstallMode.ON_NEXT_RESUME, minimumBackgroundDuration: 300 }); // Optional on restart, mandatory on resume codePush.sync({ mandatoryInstallMode: codePush.InstallMode.ON_NEXT_RESUME }); // Custom dialog codePush.sync({ updateDialog: { title: "An update is available!", appendReleaseDescription: true, descriptionPrefix: "\n\nChange log:\n" }, installMode: codePush.InstallMode.IMMEDIATE }); ``` #### With callbacks ```javascript codePush.sync( { updateDialog: true }, (status) => { if (status === codePush.SyncStatus.DOWNLOADING_PACKAGE) { showDownloadModal(); } else if (status === codePush.SyncStatus.INSTALLING_UPDATE) { hideDownloadModal(); } }, ({ receivedBytes, totalBytes }) => { updateProgress(receivedBytes / totalBytes); } ); ``` #### Return values | Status | Value | Description | |--------|-------|-------------| | `UP_TO_DATE` | 0 | App is up-to-date | | `UPDATE_INSTALLED` | 1 | Update installed, will apply based on InstallMode | | `UPDATE_IGNORED` | 2 | User ignored optional update | | `SYNC_IN_PROGRESS` | 4 | Another sync is already running | --- ## Package Objects ### LocalPackage Represents a downloaded/installed update. Get via `getUpdateMetadata` or `RemotePackage.download`. #### Properties | Property | Type | Description | |----------|------|-------------| | `appVersion` | String | Target binary version | | `deploymentKey` | String | Deployment key used | | `description` | String | Release description | | `failedInstall` | Boolean | Was this update rolled back before? | | `isFirstRun` | Boolean | First run after install? | | `isMandatory` | Boolean | Is mandatory update? | | `isPending` | Boolean | Installed but not yet applied? | | `label` | String | Internal label (e.g., `v5`) | | `packageHash` | String | SHA hash | | `packageSize` | Number | Size in bytes | #### Methods ```javascript await localPackage.install( installMode?: InstallMode, // default: ON_NEXT_RESTART minimumBackgroundDuration?: number // default: 0 ): Promise; ``` ### RemotePackage Represents an available update on the server. Get via `checkForUpdate`. Inherits all `LocalPackage` properties plus: | Property | Type | Description | |----------|------|-------------| | `downloadUrl` | String | URL to download the update | #### Methods ```javascript const localPackage = await remotePackage.download( downloadProgressCallback?: (progress: DownloadProgress) => void ): Promise; ``` --- ## Enums ### InstallMode | Value | Code | Description | |-------|------|-------------| | `IMMEDIATE` | 0 | Install and restart immediately | | `ON_NEXT_RESTART` | 1 | Install now, apply on natural restart *(default)* | | `ON_NEXT_RESUME` | 2 | Install now, apply when app resumes from background | | `ON_NEXT_SUSPEND` | 3 | Install while in background (after `minimumBackgroundDuration`) | ### CheckFrequency | Value | Code | Description | |-------|------|-------------| | `ON_APP_START` | 0 | Check on app process start | | `ON_APP_RESUME` | 1 | Check when app returns from background | | `MANUAL` | 2 | Only check when `sync()` is called | ### SyncStatus | Value | Code | Description | |-------|------|-------------| | `UP_TO_DATE` | 0 | App is up-to-date | | `UPDATE_INSTALLED` | 1 | Update installed | | `UPDATE_IGNORED` | 2 | User ignored optional update | | `UNKNOWN_ERROR` | 3 | Unknown error occurred | | `SYNC_IN_PROGRESS` | 4 | Another sync is running | | `CHECKING_FOR_UPDATE` | 5 | Querying server | | `AWAITING_USER_ACTION` | 6 | Waiting for user response to dialog | | `DOWNLOADING_PACKAGE` | 7 | Downloading update | | `INSTALLING_UPDATE` | 8 | Installing update | ### UpdateState | Value | Code | Description | |-------|------|-------------| | `RUNNING` | 0 | Currently running update | | `PENDING` | 1 | Installed but pending restart | | `LATEST` | 2 | Latest available (running or pending) | # Expo Setup Guide (/docs/setup/expo-setup) ### Installation ```bash pnpm add @turbopush/react-native-code-push @turbopush/turbopush-expo-plugin expo-build-properties ``` ### Expo Plugin Setup 1. Add the following to your `app.json` or `app.config.ts` file: > Turbopush SDK requires a minimum deployment target of 15.5, if you're using a higher version, you can ignore the expo-build-properties plugin. ```json { "plugins": [ [ "expo-build-properties", { "ios": { "deploymentTarget": "15.5", }, }, ], [ "@turbopush/turbopush-expo-plugin", { "android": { "CodePushDeploymentKey": "YOUR_ANDROID_CODE_PUSH_KEY", // generated in the create app step }, "ios": { "CodePushDeploymentKey": "YOUR_IOS_CODE_PUSH_KEY", // generated in the create app step }, } ] ] } ``` > For the sake of simplicity, we're using the deployment key hardcoded in the `app.json` file. However, we recommend using environment variables to store your deployment keys. e.g. `process.env.EXPO_PUBLIC_TURBOPUSH_ANDROID_KEY` and `process.env.EXPO_PUBLIC_TURBOPUSH_IOS_KEY`. ### Prebuild Run this command to generate the native code for your app. ```bash expo prebuild ``` ### Basic JavaScript configuration In your main app file: ```javascript title="App.tsx" import codePush from "@turbopush/react-native-code-push"; // [!code ++] const App = () => { // Your app component }; export default App; // [!code --] export default codePush(App); // [!code ++] ``` For advanced configuration, refer to the [JavaScript API Reference](/docs/setup/api-js). ## 6. First Release Creating a Release (Expo) Use the specific command that automatically generates the bundle: ```bash turbopush release-expo myapp-ios ios "1.0.0" ``` or ```bash turbopush release-expo myapp-android android "1.0.0" ``` ### Important release parameters | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `--deploymentName`, `-d` | No | `Staging` | Specifies the deployment | | `--description`, `--des` | No | - | Description of changes | | `--mandatory`, `-m` | No | `false` | Marks as mandatory update | | `--rollout`, `-r` | No | `100` | Sets rollout percentage | ### Example with parameters ```bash turbopush release-expo myapp-ios ios "1.0.0" \\ --deploymentName "Production" \\ --description "Critical bug fixes" \\ --mandatory \\ --rollout 50 ``` Read more about [Releasing Updates](/docs/cli/releasing-updates/expo). # iOS Setup (/docs/setup/ios) Once you've acquired the CodePush plugin, you need to integrate it into the Xcode project of your React Native app and configure it correctly. To do this, take the following steps: 1. Run `cd ios && pod install && cd ..` to install the necessary CocoaPods dependencies. 2. Change bundleUrl on AppDelegate file. **Swift** - Open up the `ios/[ProjectName]/AppDelegate.swift` file, and add an import statement for the CodePush headers; - Find the following line of code, which sets the source URL for bridge for production release and replace it with the following line; ```swift import React import CodePush // [!code ++] override func bundleURL() -> URL? { #if DEBUG RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") #else Bundle.main.url(forResource: "main", withExtension: "jsbundle") // [!code --] CodePush.bundleURL() // [!code ++] #endif } ``` **Objective C** - Open up the `ios/[ProjectName]/AppDelegate.m` file, and add an import statement for the CodePush headers; - Find the following line of code, which sets the source URL for bridge for production release and replace it with the following line: ```objc #import "AppDelegate.h" #import // [!code ++] - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; // [!code --] return [CodePush bundleURL]; // [!code ++] #endif } ``` 3. Add the CodePushDeploymentKey key (generated in the create app step) in the file `ios/[ProjectName]/Info.plist` ```xml CodePushDeploymentKey YOUR_IOS_DEPLOYMENT_KEY ``` 4. Certify that your `deployment_target` is 15.5 or higher in your `ios/Podfile` file. ```ruby platform :ios, '15.5' ``` ### Code Signing setup (optional) Starting with CLI version **2.1.0** you can self sign bundles during release and verify its signature before installation of update. For more info about Code Signing please refer to [relevant documentation section](../cli/code-signing). In order to configure Public Key for bundle verification you need to add record in `Info.plist` with name `CodePushPublicKey` and string value of public key content. Example: ```xml CodePushPublicKey -----BEGIN PUBLIC KEY----- MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANkWYydPuyOumR/sn2agNBVDnzyRpM16NAUpYPGxNgjSEp0etkDNgzzdzyvyl+OsAGBYF3jCxYOXozum+uV5hQECAwEAAQ== -----END PUBLIC KEY----- ``` # Expo Release (/docs/cli/releasing-updates/expo) Auto-bundles and releases Expo apps with Hermes support. ```bash turbopush release-expo ``` ## Example ```bash turbopush release-expo myapp-ios ios "1.0.0" ``` ## Parameters | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `appSlug` | Yes | - | The app slug from `npx turbopush app add` | | `platform` | Yes | - | `android` or `ios` | | `targetBinaryVersion` | Yes | - | Version found in `app.json` or `app.config.js` at build time. Supports [semver ranges](https://github.com/npm/node-semver#advanced-range-syntax) | | `--deploymentName`, `-d` | No | `Staging` | Target deployment | | `--description`, `--des` | No | - | "Change log" sent to the client, you can use it to describe the changes made to the app in this release and show it to the users if you want | | `--disabled`, `-x` | No | `false` | Prevent downloads; set to `true` to prevent clients from receiving the update until enabled | | `--mandatory`, `-m` | No | `false` | Mark as mandatory; set to `true` to require clients to install this update | | `--noDuplicateReleaseError` | No | `false` | If `true`, only warn (do not error) on duplicate content releases | | `--rollout`, `-r` | No | `100` | Percentage (1-100) of users who will receive this update | | `--outputDir`, `-o` | No | temp folder | Directory where the update bundle and metadata will be output | | `--sourcemap`, `-s` | No | `false` | If `true`, will generate sourcemaps | | `--useHermes`, `-h` | No | auto-detected | Force enable Hermes usage; auto-detected from config if omitted | # React Native Release (/docs/cli/releasing-updates/react-native) Automatically bundles and releases React Native apps. Runs `react-native bundle` internally. ```bash turbopush release-react ``` ## Quick Example Just run: ```bash turbopush release-react myapp-ios ios ``` ### Example: Specifying the version manually If you don't want Turbopush to infer the version from your project files, you can manually specify it with the `--targetBinaryVersion` flag: ```bash turbopush release-react myapp-ios ios --targetBinaryVersion "1.2.3" ``` You can also use version ranges with this flag: ```bash turbopush release-react myapp-android android --targetBinaryVersion ">=1.2.0 <2.0.0" ``` In this case, the value you provide will be used and Turbopush **will not attempt to read the version automatically** from your configuration files. ## Parameters | Parameter | Required | Default | Description | |-----------|:--------:|:-------:|-------------| | `appSlug` | Yes | - | The app slug from `npx turbopush app add` | | `platform` | Yes | - | `android` or `ios` — determines bundling behavior | | `--deploymentName`, `-d` | No | `Staging` | Target deployment | | `--description`, `--des` | No | - | "Change log" sent to the client | | `--disabled`, `-x` | No | `false` | Prevents download until activated via patch or dashboard | | `--mandatory`, `-m` | No | `false` | Marks update as required | | `--noDuplicateReleaseError` | No | `false` | Warns instead of errors on duplicate content | | `--rollout`, `-r` | No | `100` | Percentage of users (1-100) eligible for this update | | `--targetBinaryVersion`, `-t` | No | auto | Binary version(s) this update targets. Supports [semver ranges](https://github.com/npm/node-semver#advanced-range-syntax). Auto-detected from `Info.plist` (iOS) or `build.gradle` (Android) | | `--bundleName`, `-b` | No | auto | Output bundle name. Default: `main.jsbundle` (iOS), `index.android.bundle` (Android) | | `--development`, `--dev` | No | `false` | Unminified dev build | | `--entryFile`, `-e` | No | auto | Entry JS file. Default: `index.ios.js`/`index.android.js` or `index.js` | | `--gradleFile`, `-g` | No | - | Custom `build.gradle` path (Android) | | `--plistFile`, `-p` | No | - | Custom `Info.plist` path (iOS) | | `--plistFilePrefix`, `--pre` | No | - | Plist prefix for env-specific files (e.g., `STAGING` → `STAGING-Info.plist`) | | `--sourcemapOutput`, `-s` | No | - | Sourcemap output path | | `--outputDir`, `-o` | No | temp | Output directory of generated bundle, sourcemaps and assets | | `--privateKeyPath`, `-k` | No | - | Path to private key for [code signing](../code-signing) |