DEV Community

Paradane
Paradane

Posted on

How to Self-Host OTA Updates for React Native Apps

How to Self-Host OTA Updates for React Native Apps

Mobile app developers know the frustration: a critical bug fix sits in app store review while users struggle, or Microsoft App Center’s sunset leaves your OTA pipeline in limbo. Over-the-air (OTA) updates allow you to push JavaScript and asset changes directly to users without a full app store submission, bypassing the multi-day review process. With the deprecation of App Center CodePush, self-hosting has emerged as the reliable, low-cost alternative. By running your own OTA server, you gain complete control over update frequency, rollout targeting, and data privacy—all while cutting ongoing service fees. Enter Codemagic Patch: an open-source tool that simplifies building and deploying OTA patches to your own infrastructure, whether on AWS S3, DigitalOcean Spaces, or any cloud storage. This step-by-step guide walks you through setting up a self-hosted OTA update pipeline using Codemagic Patch, from server configuration to client integration, so you can ship fixes faster and keep full ownership of your update process.

Understanding OTA Updates in React Native

Over-the-air (OTA) updates allow you to modify your React Native app’s JavaScript bundle and assets without going through the standard app store review process. Instead of releasing a new binary via the App Store or Google Play, you push updated code directly to users’ devices. This capability is especially valuable for fixing critical bugs, updating content, or rolling out small features quickly.

The most well-known OTA framework for React Native is CodePush, originally developed by Microsoft. CodePush acts as a hosted service where you upload updated bundles, and the client SDK checks for, downloads, and applies the update on the next app launch. It integrates tightly with React Native’s bridge, meaning only the JS layer is replaced—the native code remains unchanged. This makes OTA updates fast and low-risk.

Benefits of OTA Updates

  • Instant fixes: A critical bug can be patched in minutes, not days. No waiting for app review.
  • No user friction: Updates happen silently in the background or on app restart.
  • Controlled rollouts: You can target specific user segments or device versions.
  • Reduced store dependency: Avoid the risk of rejection for minor changes.

Limitations of the App Store Model

Traditional app store releases require a full binary submission, which can take hours to days for review. Apple especially enforces strict guidelines, and even a small UI tweak can lead to rejection or delays. Moreover, users must manually download the update, leading to fragmentation across versions. OTA updates solve these problems by keeping the binary unchanged and updating only the script layer.

The Shift Toward Self-Hosted Solutions

When Microsoft deprecated App Center (which included the CodePush service), many teams faced a dilemma: migrate to a paid SaaS alternative or build their own update pipeline. Self-hosting has become popular because it gives you full control over distribution, data privacy, and costs. With tools like Codemagic Patch, you can set up a private OTA server with minimal overhead—using your own cloud storage (e.g., AWS S3) and CI/CD pipeline. This approach eliminates subscription fees and ensures compliance with enterprise data policies, all while retaining the same update flow that CodePush provided.

Prerequisites and Environment Setup

Before building your self-hosted OTA pipeline, ensure you have the following tools and accounts ready.

Node.js and React Native CLI – Your React Native project must be running on Node.js 18 or later. Install the React Native CLI globally if you haven't already: npm install -g @react-native-community/cli. Verify with node --version and npx react-native --version.

Codemagic CLI – Codemagic Patch is managed via its CLI. Install it globally: npm install -g codemagic-patch-cli. This tool handles bundling, signing, and uploading updates to your server.

Cloud Storage Bucket – You need a storage backend for hosting update bundles. AWS S3 is a popular choice, but any S3-compatible service (Google Cloud Storage, DigitalOcean Spaces) works. Create a private bucket (e.g., myapp-ota-updates) and note your access key and secret key.

Environment Variables – The Codemagic CLI reads configuration from environment variables. Set these in your local shell or CI/CD pipeline:

  • CM_PATCH_ACCESS_KEY – your cloud storage access key.
  • CM_PATCH_SECRET_KEY – your cloud storage secret key.
  • CM_PATCH_BUCKET – the bucket name.
  • CM_PATCH_REGION – e.g., us-east-1.
  • CM_PATCH_APP_VERSION – the current app version (optional but recommended).

Code Signing Keys – OTA updates for iOS and Android require the same signing keys used for the original app store build. Keep your iOS distribution certificate and Android keystore accessible. The CLI will prompt for these during patch publishing.

With these prerequisites in place, you are ready to configure the Codemagic Patch self-hosted server in the next step.

Setting Up Codemagic Patch Self-Hosted Server

With your environment prepared, we can now configure the heart of the self-hosted OTA pipeline: the Codemagic Patch server. This setup will allow you to build and deploy patches directly to your own cloud storage, bypassing any third-party update service.

1. Configure codemagic.yaml

First, create a codemagic.yaml file at the root of your React Native project. This file defines the workflow for building and publishing patches. Below is a minimal example that triggers on pushes to a release branch:

workflows:
  patch:
    name: Build and Deploy OTA Patch
    environment:
      vars:
        CODEMAGIC_PATCH_TOKEN: ${CODEMAGIC_PATCH_TOKEN}
        STORAGE_BUCKET: ${STORAGE_BUCKET}
    scripts:
      - name: Install dependencies
        script: npm install
      - name: Build patch
        script: |
          npx codemagic-patch build \
            --platform android \
            --output ./patches/android
      - name: Deploy patch
        script: |
          npx codemagic-patch deploy \
            --source ./patches/android \
            --destination s3://${STORAGE_BUCKET}/patches/android \
            --version 1.0.1
    artifacts:
      - ./patches/**/*.zip
Enter fullscreen mode Exit fullscreen mode

Key details:

  • CODEMAGIC_PATCH_TOKEN: An API token generated from the Codemagic dashboard – necessary for authenticating patch builds.
  • STORAGE_BUCKET: Your cloud storage bucket name (e.g., AWS S3 or Google Cloud Storage).
  • The build command compiles the JavaScript bundle and generates a differential update. The --platform flag targets either android or ios.
  • The deploy command uploads the patch archive to your specified storage location. The --version parameter allows you to tag the update (e.g., 1.0.1).

2. Patch Command Flags Explained

The codemagic-patch CLI offers several flags to control the update:

  • --platform : android or ios
  • --output : local path where the patch bundle will be written
  • --entry-file : (optional) path to your app entry point (default index.js)
  • --source : local patch file to upload
  • --destination : remote storage URL (e.g., s3://bucket/path)
  • --version : semantic version string for the patch
  • --min-app-version : minimum app version that can accept this patch (used for breaking changes)

3. Deployment Script Example

For repeatable deployments, create a shell script that chains the build and deploy steps. This script can be run locally or inside a CI pipeline:

#!/bin/bash

set -e

PLATFORM=$1
VERSION=$2

if [ -z "$PLATFORM" ] || [ -z "$VERSION" ]; then
  echo "Usage: ./deploy-patch.sh <android|ios> <version>"
  exit 1
fi

npx codemagic-patch build --platform $PLATFORM --output ./patches/$PLATFORM
npx codemagic-patch deploy \
  --source ./patches/$PLATFORM \
  --destination s3://$STORAGE_BUCKET/patches/$PLATFORM \
  --version $VERSION

echo "Patch $VERSION deployed for $PLATFORM"
Enter fullscreen mode Exit fullscreen mode

Inject environment variables like $STORAGE_BUCKET through your CI system (e.g., Codemagic environment variables) to keep credentials secure.

4. Secure and Version Your Updates

  • Security: Always sign your patch bundles using a private key. Codemagic Patch supports signing with RSA keys – ensure the public key is embedded in your React Native app (client-side verification is covered in the next section).
  • Versioning: Maintain a clear versioning strategy. Codemagic Patch allows you to specify both a patch version (e.g., 1.0.1) and a minimum compatible app version (--min-app-version). This prevents older app versions from receiving patches that depend on newer native code.
  • Storage Access: Use a pre-signed URL or temporary credentials to limit direct access to your storage bucket. Never expose permanent API keys in client-side code.

By the end of this step, you will have a pipeline that automatically builds patches and pushes them to your own server. The next section will show how the React Native app retrieves and applies these updates.

Integrating OTA Client Side in React Native

With the server configured to host and serve update bundles, the next step is to wire the React Native app so it can fetch and apply those updates. The Codemagic Patch client SDK handles version checking, download, and installation. You only need to integrate it into your app's startup sequence.

Start by installing the SDK in your React Native project:

npm install codemagic-patch
Enter fullscreen mode Exit fullscreen mode

Then, import the necessary functions in your main component (typically App.tsx or index.js):

import React, { useEffect } from 'react';
import { checkForUpdate, downloadUpdate, restartApp } from 'codemagic-patch';
Enter fullscreen mode Exit fullscreen mode

Checking for Updates on App Start

The SDK provides a checkForUpdate function that contacts your self-hosted server (the URL you configured in Codemagic Patch) and compares the latest available bundle version with the one currently installed. The result tells you whether an update is available.

Handling Update Download and Install

If an update is available, you can download it using downloadUpdate. After the download completes, call restartApp to apply the new bundle. This restart is instantaneous and does not require a full app store resubmission.

Here is a complete example component that runs the update cycle on mount:

import React, { useEffect, useState } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
import { checkForUpdate, downloadUpdate, restartApp } from 'codemagic-patch';

const App = () => {
  const [status, setStatus] = useState('Checking for updates...');

  useEffect(() => {
    const performUpdate = async () => {
      try {
        const update = await checkForUpdate();
        if (update) {
          setStatus('Downloading update...');
          await downloadUpdate(update);
          setStatus('Installing update...');
          await restartApp();
        } else {
          setStatus('No update available.');
        }
      } catch (error) {
        console.error('OTA update failed:', error);
        setStatus('Update check failed.');
      }
    };

    performUpdate();
  }, []);

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>{status}</Text>
      {status === 'Downloading update...' && <ActivityIndicator />}
    </View>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Important Considerations

  • The restartApp function triggers a reload of the JavaScript bundle, which is seamless on most devices.
  • Always handle errors gracefully so that users are not left on a broken screen if the network or server is unreachable.
  • For production apps, consider adding a progress indicator and a fallback that shows the current version if the update fails.

This client-side integration, combined with the server you set up in Section 4, completes the self-hosted OTA pipeline. In the next section, you will test the full flow and learn how to roll out updates safely.

Testing and Rolling Out Updates

Once your self-hosted OTA server is configured and the client SDK is integrated, it's time to test the pipeline thoroughly before pushing updates to all users. Start by testing in a development environment where you can manually trigger updates and verify that the bundle is correctly downloaded and applied. Use Codemagic Patch’s version targeting to send updates only to specific app versions. For example, you can release a patch targeting version 1.2.0 by specifying the target version in your codemagic.yaml:

- command: codemagic patch release --target 1.2.0 --app-version 1.2.1
Enter fullscreen mode Exit fullscreen mode

This ensures you can validate the update on a device running that exact version without affecting others. Next, set up a staging environment with a separate bucket or a different app version identifier to simulate the production flow. Test rollback scenarios early. If a faulty update slips through, use the rollback command to revert quickly:

codemagic patch rollback --target 1.2.1
Enter fullscreen mode Exit fullscreen mode

This command points the client back to the previous update bundle, giving you a safety net. To monitor update success, instrument your app with analytics — track the UpdateResult callback from the Codemagic Patch SDK. Log download success, installation, and any failures. This data helps you gauge rollout safety.

For production, adopt a canary release strategy: push the update to a small percentage of users (e.g., 5%) by adding a roll-out percentage parameter in your patch command:

codemagic patch release --rollout 0.05
Enter fullscreen mode Exit fullscreen mode

Monitor crash reports and user feedback for a few hours. If no issues surface, increase the rollout gradually to 100%. This controlled approach minimizes risk while keeping your app up to date without app store delays.

Taking Your Update Pipeline to Production

You now have a fully functional self-hosted OTA update pipeline using Codemagic Patch—from server configuration to client-side integration and safe rollout strategies. The next step is to apply this setup to your real-world React Native project. Start by migrating your existing app to use the self-hosted endpoint, then gradually move update traffic from staging to production using the canary release approach described earlier. This pipeline gives you full control over versioning, rollbacks, and deployment timing, eliminating reliance on third-party services and app store review cycles.

If your project demands custom infrastructure, advanced security policies, or complex multi-environment setups, expert guidance can accelerate your progress. Paradane (https://paradane.com) provides specialized consulting to tailor this pipeline to your exact requirements. Begin implementing today and take ownership of your mobile app update strategy.

Top comments (0)