Microsoft announced the App Center retirement in April 2024 on Microsoft Learn, with the final shutdown date of March 31, 2025. CodePush, the over-the-air (OTA) update component most of us actually cared about, was bundled into that retirement. The official guidance pointed people to either Visual Studio App Center's successor tooling or to third-party services, with very little hand-holding.
This matters because the reason CodePush worked so well for so many teams was that it was free, frictionless, and battle-tested at Microsoft scale. Whatever you migrate to in 2026 will almost certainly cost money, require some infrastructure work, or both. Going in with that expectation set correctly will save you a week of grief.
The four real options I evaluated were:
- Expo Updates (EAS Update) — managed by Expo, works with bare React Native apps as of SDK 52+
- App.fly.io OTA — a newer commercial service that sprang up specifically to replace CodePush
- Microsoft Hot Update — the partially-supported successor Microsoft pointed customers to
- Self-hosted — using
react-native-ota-hot-update or rolling your own on S3/R2
Option 1: Expo Updates — the default I now recommend
If you are starting this migration today and your app is not doing anything bizarre with native modules, just go with Expo Updates. I resisted this for about two months because we were on bare React Native and I had irrational concerns about coupling to Expo's ecosystem. Those concerns were wrong.
As of Expo SDK 52 (released late 2024), EAS Update works cleanly with bare React Native projects, not just Expo-managed ones. The install is genuinely two commands:
npx expo install expo-updates
eas update:configure
For the bare workflow you also need to wire the native side. On iOS that means adding the EXUpdatesURL and EXUpdatesRuntimeVersion keys to Info.plist, and on Android updating strings.xml. The EAS CLI generates the right values for you, but here is what the iOS side ends up looking like:
<key>EXUpdatesURL</key>
<string>https://u.expo.dev/your-project-id</string>
<key>EXUpdatesRuntimeVersion</key>
<string>1.0.0</string>
<key>EXUpdatesCheckOnLaunch</key>
<string>ALWAYS</string>
Pushing an update is one command, and you can target release channels exactly like CodePush did:
eas update --branch production --message "Fix checkout button alignment on iOS 26"
The runtime API is also closer to CodePush than I expected. Here is the equivalent of the old codePush.sync() call I had been running on app foreground:
import * as Updates from 'expo-updates';
import { AppState } from 'react-native';
AppState.addEventListener('change', async (nextState) => {
if (nextState !== 'active') return;
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}
} catch (e) {
// Swallow silently — never block app launch on OTA failure
console.warn('OTA check failed', e);
}
});
Cost as of 2026: the free tier covers 1,000 monthly active updates, which is fine for tiny apps and useless for anything real. The Production plan is $99/month and includes 200,000 MAU. For one of my apps with about 80k MAU we ended up on the Enterprise tier at $999/month because we wanted SOC 2 reports and longer log retention, but that is a choice you can defer.
Migration cost from CodePush: roughly two engineer-days per app. Most of that time was spent rewriting the deployment scripts in CI to call eas update instead of appcenter codepush release-react, and adjusting the staged rollout logic.
Option 2: App.fly.io — the spiritual CodePush successor
App.fly.io (no relation to Fly.io the hosting company, confusingly) launched in late 2024 specifically to fill the CodePush gap. The API surface is deliberately CodePush-shaped, and they even provide a compatibility shim that lets you keep most of your existing react-native-code-push call sites without rewriting them. That alone made it tempting.
The integration looks like this:
import { AppFly } from 'react-native-appfly-update';
const config = {
deploymentKey: process.env.APPFLY_PRODUCTION_KEY,
serverUrl: 'https://api.app.fly.io',
installMode: AppFly.InstallMode.ON_NEXT_RESUME,
mandatoryInstallMode: AppFly.InstallMode.IMMEDIATE,
};
export default AppFly(config)(App);
I genuinely liked how close this felt to the old API. Their release tooling also mirrored CodePush:
appfly release-react MyApp ios \
--deployment-name Production \
--target-binary-version "2.4.0" \
--rollout 25
Where App.fly.io lost me was the pricing curve and the fact that the binary diffs are noticeably larger than EAS Update's. For a 4 MB JS bundle change my updates were averaging around 1.1 MB on the wire, versus around 600 KB on EAS. Their pricing also jumps from $49/month (50k MAU) straight to $499/month (500k MAU) with no middle tier, which is awkward if you sit around 100k MAU like I did.
Verdict: a reasonable choice if you want minimal code change and you sit comfortably inside one of their pricing buckets. Otherwise EAS wins on both economics and bundle size.
Option 3: Microsoft Hot Update — skip this one
Microsoft's official-ish migration path pointed at "Hot Update," which under the covers is essentially the App Center Distribute SDK repurposed. I spent a frustrating afternoon trying to get it working on a React Native 0.74 app and gave up. The documentation is thin, the iOS SDK has not had a meaningful release since November 2024, and the GitHub issue tracker has multiple unresolved threads about Hermes bundle compatibility on RN 0.73+.
If you are on a very old React Native version (0.68 or below) and you cannot upgrade, Hot Update might be the path of least resistance because the old CodePush SDK still works with it. For anyone on a modern RN, do not waste your time.
Option 4: Self-hosted on Cloudflare R2 (what I picked for the privacy-sensitive app)
For one client in regulated healthcare, we could not put any user-identifying telemetry on a third-party service. That ruled out both EAS and App.fly.io. We ended up self-hosting with react-native-ota-hot-update, an open-source library that handles the bundle download and atomic swap, paired with Cloudflare R2 for storage.
The architecture is dead simple:
- CI builds the JS bundle with
npx react-native bundle and uploads it to R2 under a versioned path like updates/ios/1.4.2/build-419.bundle
- A tiny Cloudflare Worker checks the requested platform + binary version against a KV store and returns the latest bundle URL or 204
- The client polls that worker on resume and downloads if a new bundle is available
The worker is genuinely about 40 lines:
export default {
async fetch(request, env) {
const url = new URL(request.url);
const platform = url.searchParams.get('platform');
const binaryVersion = url.searchParams.get('binaryVersion');
const currentHash = url.searchParams.get('currentHash');
const key = `latest:${platform}:${binaryVersion}`;
const latest = await env.UPDATES.get(key, { type: 'json' });
if (!latest || latest.hash === currentHash) {
return new Response(null, { status: 204 });
}
return Response.json({
bundleUrl: `https://cdn.example.com/${latest.path}`,
hash: latest.hash,
mandatory: latest.mandatory ?? false,
});
},
};
And the client-side handler:
import hotUpdate from 'react-native-ota-hot-update';
import { Platform } from 'react-native';
async function checkForUpdate(currentHash: string) {
const res = await fetch(
`https://updates.example.com/?platform=${Platform.OS}` +
`&binaryVersion=2.4.0¤tHash=${currentHash}`
);
if (res.status === 204) return;
const { bundleUrl, hash, mandatory } = await res.json();
await hotUpdate.downloadBundleUri(require('react-native'), bundleUrl, hash, {
restartAfterInstall: mandatory,
});
}
Cost: roughly $0.50/month in R2 storage and Workers invocations for an app with about 30k MAU. Compared to the $99/month minimum for EAS that is a huge saving, but it is offset by the engineering time.
Migration cost: about five engineer-days, including the worker, the CI scripts, integrity verification with SHA-256, and a staged rollout mechanism. You also lose the nice rollout-percentage UI, A/B testing, and built-in analytics that the managed services provide. For us that was an acceptable trade.
What I would actually pick today
If you are migrating off CodePush in 2026 and you have not already chosen, my decision tree is:
- Default to Expo Updates unless you have a specific reason not to. The DX is the best, the pricing is the most predictable, and the company has the clearest commitment to React Native.
- Pick App.fly.io if you have a large existing CodePush codebase you do not want to refactor and you fit cleanly into one of their pricing tiers.
- Self-host on R2 + Workers if you have hard data-residency requirements or you are running at a scale where $999/month for EAS Enterprise is annoying but five days of engineering work is not.
- Avoid Microsoft Hot Update unless you are stuck on a pre-0.70 React Native version.
One thing worth flagging: Apple's policy on JS-only OTA updates has remained stable in 2026 — section 3.3.2 of the App Store Review Guidelines still permits interpreted code updates as long as you do not change the documented purpose of the app. Android has never cared. So whichever option you pick, you are not going to get rejected for using OTA itself.
If you want more on related migration topics, I have written about migrating to the React Native New Architecture and the bare vs Expo workflow question in 2026, both of which interact with whichever OTA path you pick.
FAQ
Can I keep using react-native-code-push after the App Center shutdown?
The SDK still compiles and runs on older React Native versions, but the backend it talks to is gone. You would need to point it at a compatible server, which is essentially what App.fly.io's compatibility shim does. Otherwise the SDK is a no-op as of March 2025.
Does Expo Updates work with bare React Native in 2026?
Yes, fully, as of SDK 52. You install expo-updates as a standalone package, configure native files via eas update:configure, and you do not need to convert to a managed Expo project. About 60% of my EAS Update customers are on bare workflows.
How big can an OTA update be before Apple cares?
Apple does not publish a hard limit, but the practical guidance is that you should not be shipping new features or significantly changing the app's purpose via OTA. JS bug fixes, content updates, and UI tweaks are all fine. I have shipped 2 MB JS bundles via EAS Update without any review issues.
Is there a free CodePush alternative for hobby projects?
EAS Update's free tier (1,000 MAU) covers truly tiny apps. Beyond that, the cheapest path is self-hosting on Cloudflare R2 with the free Workers tier — you will pay literal pennies per month for a small app, and the only real cost is the initial setup time.
Closing thought
The CodePush shutdown was painful, but it forced the React Native ecosystem to develop multiple genuinely good OTA options where there used to be one default. A year on from the cutoff, every team I have helped migrate has ended up in a better place than they started — cheaper, faster bundle downloads, or both. If you are still putting this migration off, this is your sign to pick one of the four options above and book a week on the calendar. It is not as bad as you are imagining.