🚀 Blazing fast hot updates (OTA) for React Native — Hermes-optimized diff patches, crash rollback, staged rollout. A CodePush alternative for iOS / Android / HarmonyOS.
See the codereact-native-update is an over-the-air (OTA) hot update library for React Native, and a maintained alternative to Microsoft CodePush (retired with App Center on March 31, 2025). It ships Hermes-optimized incremental patches (a one-line fix is about 3.4 KB), rolls back crashing updates automatically, supports staged rollouts, analytics and JS error reporting with source-mapped stacks, and runs on iOS, Android and HarmonyOS, including Expo projects and the New Architecture. For full documentation, visit:
React Native New Architecture is supported.
See the docs:
Every option below is declared on ClientOptions (exported from the package
root) and documented in full at https://cresc.dev/docs/api. Runtime changes go
through client.setOptions(...).
| Option | Default | What it controls |
|---|---|---|
appKey | required | App identity on the update platform |
server | Pushy/Cresc preset | main endpoints and queryUrls for remote endpoint discovery (https only) |
updateStrategy | alertUpdateAndIgnoreError (alwaysAlert in dev) | alwaysAlert / alertUpdateAndIgnoreError / silentAndNow / silentAndLater / null |
checkStrategy | both | onAppStart / onAppResume / both / null (no automatic check; the native cold-start check still downloads but never activates) |
autoMarkSuccess | true | Mark the running update healthy from the Provider |
autoMarkSuccessDelayMs | 1000 | Delay before the automatic mark; raise it when critical modules load late |
healthCheck | – | Consulted when the auto-mark timer fires; return false to keep crash protection armed |
beforeCheckUpdate / afterCheckUpdate | – | Veto a check / observe its final state |
beforeDownloadUpdate / afterDownloadUpdate | – | Veto a download / veto the activation after it |
beforeReload | – | Veto a switchVersion / restartApp reload |
onPackageExpired | – | Veto the "binary too old" flow (downloadUrl) |
maxRetries | 3 | Download retries with jittered backoff |
overridePackageVersion | – | Report a different binary version to the server |
dismissErrorAfter | – | Auto-clear lastError after N ms |
locale | zh (Pushy) / en (Cresc) | Language of alerts and messages |
debug | false | Enable update operations in development builds and verbose SDK logging |
throwError | false | Rethrow pipeline errors to the caller (they always reach onError) |
disableTelemetry | false | Stop lifecycle events (download/patch failure, rollback, mark success) reaching the server |
disableErrorReporting | false | Stop JS exception reporting (see below) |
disableNativeCheck | false | Turn off the native cold-start check that rescues bricked devices |
nativeConfigSource | 'javascript' | Who owns the persisted native check configuration; 'native' when the host calls configure() itself, so JS never overwrites it (migration notes) |
testChannel | true | Honour __rnPushyVersionHash deep links / QR codes; set false in production builds |
logger | – | Receives every SDK event (type, data) for your own logging |
Beyond the Provider hooks, the client exposes onError(listener),
captureException(error, context), resetToPackagedBundle({ restart }),
getUpdateMetadata() / attachToSentry() / attachToCrashlytics() for crash
reporters, and useUpdateProgress() for progress bars that must not re-render
the whole tree.
The SDK reports uncaught and manually captured JavaScript errors against the
currently running OTA release. Reporting is enabled by default, best-effort,
disabled in development and when disableTelemetry is enabled, and never
replaces an existing React Native, Sentry or Crashlytics global handler.
import { Cresc } from 'react-native-update';
const client = new Cresc({ appKey: 'your-app-key' });
try {
await submitOrder();
} catch (error) {
client.captureException(error, {
extra: { screen: 'checkout', retry: 1 },
});
}
// Explicit runtime opt-out (also disables manual captureException transport):
client.setOptions({ disableErrorReporting: true });
You can also opt out at construction with
new Cresc({ appKey, disableErrorReporting: true }).
Only errors from an OTA version with a known version hash are uploaded. Context is deliberately restricted to bounded scalar values; do not include secrets or personal information.
Hermes bytecode is hostile to generic binary diffing, and we attack that at two points that stack:
bundle compiles against an earlier bytecode of the same app (hermesc -base-bytecode=…), pinning those IDs so that only real changes ever reach the diff.The bytecode is also emitted without its debug-info section — a flat 21% smaller, the same thing React Native's own release builds do — which shrinks the full package and every patch derived from it.
The numbers below are measured on real release bundles of a React Native 0.86 app (Hermes HBC v98, ~3.5 MB bytecode with debug info stripped, as shipped). Every patch is verified by an actual round-trip before its size is recorded, and every delta-mode build is verified equivalent to a plain compile. Full methodology, fixtures, and runnable code: hbc-diff-benchmark.
Hermes bytecode (.hbc) — what ships in production Hermes apps:
| Scenario | Full OTA (gzip) | bsdiff | react-native-update | vs bsdiff |
|---|---|---|---|---|
| One-line text change | 1544.6 KB | 93.7 KB | 3.4 KB | 28× smaller |
| Small feature (~60 LOC) | 1555.8 KB | 411.6 KB | 50.2 KB | 8.2× smaller |
| Medium feature (~300 LOC) | 1604.7 KB | 551.6 KB | 97.8 KB | 5.6× smaller |
Plain-text JS bundle — non-Hermes apps (same scenarios, same app):
| Scenario | Full OTA (gzip) | bsdiff | react-native-update |
|---|---|---|---|
| One-line text change | 807.2 KB | 0.3 KB | 0.1 KB |
| Small feature (~60 LOC) | 813.1 KB | 7.4 KB | 5.8 KB |
| Medium feature (~300 LOC) | 837.7 KB | 38.4 KB | 28.7 KB |
Text bundles have no offset tables and no string-table churn, so they diff cleanly on their own — patches are hundreds of bytes to tens of KB. The amplification the two optimizations above remove is a Hermes-bytecode-specific problem, which is exactly why they exist.
Highlights:
Since 10.52.1 every cold start runs one background update check that does not depend on the app bundle — the request, the download, the patch and the version switch all happen natively. It exists for exactly one reason: when the running update is broken enough that JS never starts, something still has to be able to fetch the fix. Normal updates remain the JS flow's job, and the JS check reuses this result instead of issuing its own request.
updateStrategy / checkStrategy; otherwise it is downloaded and JS decides.RCTSetFatalHandler.disableNativeCheck: true removes one background request per cold start, at the cost of giving up the recovery above — a device bricked by a bad update can no longer heal itself.| Category | react-native-update | expo-update | react-native-code-push |
|---|---|---|---|
| Price / Cost | Free tier with multiple paid plans, bandwidth included | Free tier with multiple paid plans, extra bandwidth charges apply | ❌ Discontinued (Microsoft App Center shut down on March 31, 2025) |
| Package Size | ⭐⭐⭐⭐⭐ Tens to hundreds of KB (incremental) | ⭐⭐⭐ Full bundle updates (usually tens of MB) | ❌ Discontinued |
| Hermes-specific diff | ✅ Delta-mode compile + HBC-aware transform, smallest patches | ❌ Full bundle | ❌ Discontinued |
| Staged (gray) rollout | ✅ Built-in, percentage-based | ✅ Supported | ❌ Discontinued |
| Analytics | ✅ Built-in (version distribution, success rate) | ⚠️ Limited | ❌ Discontinued |
| Release health monitoring | ✅ Built-in | ❌ Not available | ❌ Discontinued |
| Technical Support | ✅ Paid dedicated support | ⚠️ Community support | ❌ Discontinued |
| Server Deployment | ✅ Hosted service or paid private deployment | ✅ Hosted by Expo (EAS Update) | ❌ Discontinued |
| Bandwidth Usage | ⭐⭐⭐⭐⭐ Very low (incremental) | ⭐⭐⭐ Higher (full bundle) | ❌ Discontinued |
Bridge-free hosts use PushyRuntime.prepareBundle (Android),
RCTPushy.prepareBundle (Swift), or PushyFileJSBundleProvider.prepareBundle
(HarmonyOS), after normal launch bundle resolution. See the
native host API migration guide for the
source-breaking rename, unchanged JS bridge contract, and native rebuild steps.
Each platform also takes a timeout: PushyRuntime.prepareBundle(context, timeoutMillis, callback), RCTPushy.prepareBundle(timeout:completion:) and
prepareBundle(timeoutMs). On expiry the result is failed / timeout; the
round keeps running in the background and still applies to the next launch.
Without one, Android and iOS wait at most 30 minutes and HarmonyOS waits for
the round.
🚀 Blazing fast hot updates (OTA) for React Native — Hermes-optimized diff patches, crash rollback, staged rollout. A CodePush alternative for iOS / Android / HarmonyOS.
See the codereact-native-update is an over-the-air (OTA) hot update library for React Native, and a maintained alternative to Microsoft CodePush (retired with App Center on March 31, 2025). It ships Hermes-optimized incremental patches (a one-line fix is about 3.4 KB), rolls back crashing updates automatically, supports staged rollouts, analytics and JS error reporting with source-mapped stacks, and runs on iOS, Android and HarmonyOS, including Expo projects and the New Architecture. For full documentation, visit:
React Native New Architecture is supported.
See the docs:
Every option below is declared on ClientOptions (exported from the package
root) and documented in full at https://cresc.dev/docs/api. Runtime changes go
through client.setOptions(...).
| Option | Default | What it controls |
|---|---|---|
appKey | required | App identity on the update platform |
server | Pushy/Cresc preset | main endpoints and queryUrls for remote endpoint discovery (https only) |
updateStrategy | alertUpdateAndIgnoreError (alwaysAlert in dev) | alwaysAlert / alertUpdateAndIgnoreError / silentAndNow / silentAndLater / null |
checkStrategy | both | onAppStart / onAppResume / both / null (no automatic check; the native cold-start check still downloads but never activates) |
autoMarkSuccess | true | Mark the running update healthy from the Provider |
autoMarkSuccessDelayMs | 1000 | Delay before the automatic mark; raise it when critical modules load late |
healthCheck | – | Consulted when the auto-mark timer fires; return false to keep crash protection armed |
beforeCheckUpdate / afterCheckUpdate | – | Veto a check / observe its final state |
beforeDownloadUpdate / afterDownloadUpdate | – | Veto a download / veto the activation after it |
beforeReload | – | Veto a switchVersion / restartApp reload |
onPackageExpired | – | Veto the "binary too old" flow (downloadUrl) |
maxRetries | 3 | Download retries with jittered backoff |
overridePackageVersion | – | Report a different binary version to the server |
dismissErrorAfter | – | Auto-clear lastError after N ms |
locale | zh (Pushy) / en (Cresc) | Language of alerts and messages |
debug | false | Enable update operations in development builds and verbose SDK logging |
throwError | false | Rethrow pipeline errors to the caller (they always reach onError) |
disableTelemetry | false | Stop lifecycle events (download/patch failure, rollback, mark success) reaching the server |
disableErrorReporting | false | Stop JS exception reporting (see below) |
disableNativeCheck | false | Turn off the native cold-start check that rescues bricked devices |
nativeConfigSource | 'javascript' | Who owns the persisted native check configuration; 'native' when the host calls configure() itself, so JS never overwrites it (migration notes) |
testChannel | true | Honour __rnPushyVersionHash deep links / QR codes; set false in production builds |
logger | – | Receives every SDK event (type, data) for your own logging |
Beyond the Provider hooks, the client exposes onError(listener),
captureException(error, context), resetToPackagedBundle({ restart }),
getUpdateMetadata() / attachToSentry() / attachToCrashlytics() for crash
reporters, and useUpdateProgress() for progress bars that must not re-render
the whole tree.
The SDK reports uncaught and manually captured JavaScript errors against the
currently running OTA release. Reporting is enabled by default, best-effort,
disabled in development and when disableTelemetry is enabled, and never
replaces an existing React Native, Sentry or Crashlytics global handler.
import { Cresc } from 'react-native-update';
const client = new Cresc({ appKey: 'your-app-key' });
try {
await submitOrder();
} catch (error) {
client.captureException(error, {
extra: { screen: 'checkout', retry: 1 },
});
}
// Explicit runtime opt-out (also disables manual captureException transport):
client.setOptions({ disableErrorReporting: true });
You can also opt out at construction with
new Cresc({ appKey, disableErrorReporting: true }).
Only errors from an OTA version with a known version hash are uploaded. Context is deliberately restricted to bounded scalar values; do not include secrets or personal information.
Hermes bytecode is hostile to generic binary diffing, and we attack that at two points that stack:
bundle compiles against an earlier bytecode of the same app (hermesc -base-bytecode=…), pinning those IDs so that only real changes ever reach the diff.The bytecode is also emitted without its debug-info section — a flat 21% smaller, the same thing React Native's own release builds do — which shrinks the full package and every patch derived from it.
The numbers below are measured on real release bundles of a React Native 0.86 app (Hermes HBC v98, ~3.5 MB bytecode with debug info stripped, as shipped). Every patch is verified by an actual round-trip before its size is recorded, and every delta-mode build is verified equivalent to a plain compile. Full methodology, fixtures, and runnable code: hbc-diff-benchmark.
Hermes bytecode (.hbc) — what ships in production Hermes apps:
| Scenario | Full OTA (gzip) | bsdiff | react-native-update | vs bsdiff |
|---|---|---|---|---|
| One-line text change | 1544.6 KB | 93.7 KB | 3.4 KB | 28× smaller |
| Small feature (~60 LOC) | 1555.8 KB | 411.6 KB | 50.2 KB | 8.2× smaller |
| Medium feature (~300 LOC) | 1604.7 KB | 551.6 KB | 97.8 KB | 5.6× smaller |
Plain-text JS bundle — non-Hermes apps (same scenarios, same app):
| Scenario | Full OTA (gzip) | bsdiff | react-native-update |
|---|---|---|---|
| One-line text change | 807.2 KB | 0.3 KB | 0.1 KB |
| Small feature (~60 LOC) | 813.1 KB | 7.4 KB | 5.8 KB |
| Medium feature (~300 LOC) | 837.7 KB | 38.4 KB | 28.7 KB |
Text bundles have no offset tables and no string-table churn, so they diff cleanly on their own — patches are hundreds of bytes to tens of KB. The amplification the two optimizations above remove is a Hermes-bytecode-specific problem, which is exactly why they exist.
Highlights:
Since 10.52.1 every cold start runs one background update check that does not depend on the app bundle — the request, the download, the patch and the version switch all happen natively. It exists for exactly one reason: when the running update is broken enough that JS never starts, something still has to be able to fetch the fix. Normal updates remain the JS flow's job, and the JS check reuses this result instead of issuing its own request.
updateStrategy / checkStrategy; otherwise it is downloaded and JS decides.RCTSetFatalHandler.disableNativeCheck: true removes one background request per cold start, at the cost of giving up the recovery above — a device bricked by a bad update can no longer heal itself.| Category | react-native-update | expo-update | react-native-code-push |
|---|---|---|---|
| Price / Cost | Free tier with multiple paid plans, bandwidth included | Free tier with multiple paid plans, extra bandwidth charges apply | ❌ Discontinued (Microsoft App Center shut down on March 31, 2025) |
| Package Size | ⭐⭐⭐⭐⭐ Tens to hundreds of KB (incremental) | ⭐⭐⭐ Full bundle updates (usually tens of MB) | ❌ Discontinued |
| Hermes-specific diff | ✅ Delta-mode compile + HBC-aware transform, smallest patches | ❌ Full bundle | ❌ Discontinued |
| Staged (gray) rollout | ✅ Built-in, percentage-based | ✅ Supported | ❌ Discontinued |
| Analytics | ✅ Built-in (version distribution, success rate) | ⚠️ Limited | ❌ Discontinued |
| Release health monitoring | ✅ Built-in | ❌ Not available | ❌ Discontinued |
| Technical Support | ✅ Paid dedicated support | ⚠️ Community support | ❌ Discontinued |
| Server Deployment | ✅ Hosted service or paid private deployment | ✅ Hosted by Expo (EAS Update) | ❌ Discontinued |
| Bandwidth Usage | ⭐⭐⭐⭐⭐ Very low (incremental) | ⭐⭐⭐ Higher (full bundle) | ❌ Discontinued |
Bridge-free hosts use PushyRuntime.prepareBundle (Android),
RCTPushy.prepareBundle (Swift), or PushyFileJSBundleProvider.prepareBundle
(HarmonyOS), after normal launch bundle resolution. See the
native host API migration guide for the
source-breaking rename, unchanged JS bridge contract, and native rebuild steps.
Each platform also takes a timeout: PushyRuntime.prepareBundle(context, timeoutMillis, callback), RCTPushy.prepareBundle(timeout:completion:) and
prepareBundle(timeoutMs). On expiry the result is failed / timeout; the
round keeps running in the background and still applies to the next launch.
Without one, Android and iOS wait at most 30 minutes and HarmonyOS waits for
the round.