Skip to Content
DocumentationSDKReact Native

React Native SDK

react-native-appspacer — A production-grade OTA update SDK for React Native. Push JavaScript bundle updates directly to your users’ devices with enterprise-level reliability and safety.


Overview

The AppSpacer SDK integrates into your React Native app to enable Over-The-Air updates. Once integrated, your app can:

  • Check the AppSpacer server for new releases
  • Download update packages with integrity verification
  • Install bundles atomically — no partial updates, ever
  • Roll back automatically if an update crashes on boot
  • Display beautiful, themed update prompts with zero UI code

Requirements

DependencyMinimum Version
React Native>= 0.72
React>= 18.0
iOS deployment target13.0
Android minSdk23
AppSpacer backendRunning (self-hosted or cloud)

Installation

npm install react-native-appspacer

iOS

cd ios && pod install && cd ..

Android

No additional steps — the package is auto-linked via React Native CLI.


Native Setup

The fastest way to configure native files is one command:

npx appspacer setup

This auto-detects your project structure, identifies your React Native architecture, and injects the correct bundle resolver. See the CLI docs for all options.

After running setup, skip to SDK Initialization below.


Manual — Android

New Architecture (React Native 0.73+)

Uses ReactHostDelegate / ReactHostImpl to resolve OTA bundles dynamically.

// MainApplication.kt import com.facebook.react.ReactHostDelegate import com.facebook.react.ReactHostImpl override val reactHost: ReactHost by lazy { val delegate = object : ReactHostDelegate { override fun getJsBundleFilePath(): String? = com.appspacer.AppSpacerModule.getCustomBundlePath(applicationContext) override fun getJsMainModulePath(): String = "index" override fun getReactPackages(): List<ReactPackage> = PackageList(this@MainApplication).packages } ReactHostImpl(applicationContext, delegate, SurfaceDelegateFactory { null }, true, true) }

Traditional Architecture (React Native < 0.73)

Uses ReactNativeHost to override the JS bundle file path.

// MainApplication.kt override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { override fun getJSBundleFile(): String? = com.appspacer.AppSpacerModule.getCustomBundlePath(applicationContext) override fun getJSMainModuleName(): String = "index" override fun getPackages(): List<ReactPackage> = PackageList(this@MainApplication).packages override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG }

How it works: getCustomBundlePath checks for a downloaded OTA bundle at <internal-storage>/AppSpacerOTA/current_bundle/index.android.bundle. If it doesn’t exist, the default binary bundle is used.


Manual — iOS

Objective-C (AppDelegate.mm)

#import <AppSpacerModule.h> - (NSURL *)bundleURL { return [AppSpacerModule bundleURL]; }

Swift (AppDelegate.swift)

import AppSpacerModule override func bundleURL() -> URL? { return AppSpacerModule.bundleURL() }

How it works: Returns the downloaded OTA bundle path if one exists on disk. Otherwise, falls back to the binary-embedded main.jsbundle.


SDK Initialization

Call AppSpacer.init() once at app startup, before any update checks:

import { AppSpacer } from 'react-native-appspacer'; AppSpacer.init({ apiUrl: 'https://your-api.example.com/api', deploymentKey: 'sk_YOUR_DEPLOYMENT_KEY', appVersion: '1.0.0', });

Configuration Options

ParameterTypeRequiredDefaultDescription
apiUrlstringhttps://api.appspacer.com/apiYour AppSpacer backend URL
deploymentKeystringDeployment key from the dashboard (e.g. sk_xxxx)
appVersionstringCurrent native app version — must match target_version of your releases
checkIntervalnumber0Auto-check interval in seconds. 0 = manual only

Auto-check every 5 minutes:

AppSpacer.init({ apiUrl: 'https://your-api.example.com/api', deploymentKey: 'sk_YOUR_DEPLOYMENT_KEY', appVersion: '1.0.0', checkInterval: 300, });

Usage Patterns

Pattern 1: Zero-Config HOC (Easiest)

Wrap your root component with withAppSpacer for a fully managed experience. This handles initialization, update checking, beautiful update dialogs, and installation — all automatically.

import { withAppSpacer, InstallMode, UpdateUIType } from 'react-native-appspacer'; const App = () => { return <MainNavigator />; }; export default withAppSpacer({ deploymentKey: 'sk_YOUR_DEPLOYMENT_KEY', appVersion: '1.0.0', updateDialog: true, // Show premium themed update prompt updateUI: UpdateUIType.GLASS, // Choose from 5 visual themes installMode: InstallMode.IMMEDIATE, // Restart immediately after install })(App);

Built-in UI Themes

AppSpacer ships with 5 hand-crafted UI themes for the update dialog — no UI code required:

ThemeEnum ValueDescription
Premium DarkUpdateUIType.PREMIUM_DARKSleek dark mode with vibrant glow effects (default)
GlassUpdateUIType.GLASSModern glassmorphism with frosted transparency
Bottom SheetUpdateUIType.BOTTOM_SHEETiOS-inspired slide-up sheet from the bottom
MinimalUpdateUIType.MINIMALClean, utility-focused light theme
Full ScreenUpdateUIType.FULL_SCREENImmersive, bold experience for major updates

withAppSpacer Options

OptionTypeDefaultDescription
deploymentKeystringRequiredYour SDK deployment key
appVersionstringRequiredNative app version (e.g. 1.2.0)
updateDialogbooleanfalseWhether to show the premium popup
updateUIUpdateUITypePREMIUM_DARKThe visual theme to use
installModeInstallModeON_NEXT_RESTARTWhen to apply the update
apiUrlstringhttps://api.appspacer.com/apiYour backend URL

Pattern 2: Silent Auto-Update

Check, download, and install silently with immediate restart:

import { useEffect } from 'react'; import { AppSpacer, InstallMode } from 'react-native-appspacer'; function App() { useEffect(() => { AppSpacer.sync({ installMode: InstallMode.IMMEDIATE }); }, []); return <MainApp />; }

Pattern 3: Install on Next Restart

Download silently in the background, apply on next cold start:

import { useEffect } from 'react'; import { AppSpacer, InstallMode } from 'react-native-appspacer'; function App() { useEffect(() => { AppSpacer.sync({ installMode: InstallMode.ON_NEXT_RESTART }); }, []); return <MainApp />; }

Pattern 4: Install on App Resume

Download immediately, apply when the user returns to the app from the background:

import { useEffect } from 'react'; import { AppSpacer, InstallMode } from 'react-native-appspacer'; function App() { useEffect(() => { AppSpacer.sync({ installMode: InstallMode.ON_NEXT_RESUME }); }, []); return <MainApp />; }

Pattern 5: Full UI Control

Build your own update experience using the useAppSpacerUpdate React hook:

import { useEffect } from 'react'; import { View, Text, Button, ActivityIndicator } from 'react-native'; import { useAppSpacerUpdate, UpdateStatus } from 'react-native-appspacer'; function App() { const { status, update, error, checkForUpdate, downloadUpdate, restartApp } = useAppSpacerUpdate(); useEffect(() => { checkForUpdate(); }, []); if (status === UpdateStatus.CHECKING) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <ActivityIndicator size="large" /> <Text>Checking for updates...</Text> </View> ); } if (status === UpdateStatus.UPDATE_AVAILABLE && update) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text style={{ fontSize: 18, fontWeight: 'bold' }}>Update Available</Text> <Text>{update.description}</Text> {update.packageSize && ( <Text style={{ color: '#666' }}> Size: {(update.packageSize / 1024 / 1024).toFixed(1)} MB </Text> )} <Button title="Download & Install" onPress={downloadUpdate} /> </View> ); } if (status === UpdateStatus.DOWNLOADING) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <ActivityIndicator size="large" /> <Text>Downloading update...</Text> </View> ); } if (status === UpdateStatus.INSTALLED) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text style={{ fontSize: 18 }}>✅ Update Ready</Text> <Button title="Restart to Apply" onPress={restartApp} /> </View> ); } if (status === UpdateStatus.ERROR) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text style={{ color: 'red' }}>Update error: {error}</Text> <Button title="Retry" onPress={checkForUpdate} /> </View> ); } return <MainApp />; }

Pattern 6: Mandatory Updates

When you push a mandatory release from the CLI (--mandatory), the SDK auto-detects it and uses IMMEDIATE install mode regardless of your default:

import { useEffect } from 'react'; import { AppSpacer } from 'react-native-appspacer'; function App() { useEffect(() => { // Mandatory updates → auto IMMEDIATE // Optional updates → ON_NEXT_RESTART AppSpacer.sync(); }, []); return <MainApp />; }

React Hook API

useAppSpacerUpdate()

A React hook that provides reactive state and actions for managing updates.

import { useAppSpacerUpdate } from 'react-native-appspacer'; const { status, // UpdateStatus — current state of the update process update, // AppSpacerUpdate | null — info about available update error, // string | null — error message when status is ERROR checkForUpdate, // () => Promise<void> — check server for updates downloadUpdate, // () => Promise<void> — download and verify pending update restartApp, // () => void — reload app with new bundle sync, // (options?) => Promise<void> — full lifecycle helper } = useAppSpacerUpdate();

Core API Reference

AppSpacer.init(config)

Initialize the SDK. Must be called once before any other method.

AppSpacer.init({ apiUrl: 'https://your-api.example.com/api', deploymentKey: 'sk_YOUR_KEY', appVersion: '1.0.0', checkInterval: 0, });

AppSpacer.checkForUpdate()

Check the server for available updates. Returns an AppSpacerUpdate object.

const update = await AppSpacer.checkForUpdate(); if (update.updateAvailable) { console.log('New version:', update.label); console.log('Description:', update.description); console.log('Mandatory:', update.mandatory); console.log('Size:', update.packageSize, 'bytes'); }

AppSpacer.downloadUpdate()

Download, verify (SHA-256), and install the pending update. Must call checkForUpdate() first.

await AppSpacer.downloadUpdate(); // Update is now on disk, verified, and ready — restart to apply

AppSpacer.sync(options?)

High-level helper that combines check → download → install → restart into a single call.

// Auto-restart immediately await AppSpacer.sync({ installMode: InstallMode.IMMEDIATE }); // Apply on next cold start await AppSpacer.sync({ installMode: InstallMode.ON_NEXT_RESTART }); // Apply when app comes back from background await AppSpacer.sync({ installMode: InstallMode.ON_NEXT_RESUME }); // Default: IMMEDIATE for mandatory, ON_NEXT_RESTART for optional await AppSpacer.sync();

SyncOptions

OptionTypeDefaultDescription
installModeInstallModeON_NEXT_RESTARTWhen to apply the update
mandatoryInstallModeInstallModeIMMEDIATEOverride for mandatory updates
updateDialogboolean | UpdateDialogOptionsfalseShow a native prompt
updateUIUpdateUITypePREMIUM_DARKVisual theme for the dialog
minimumBackgroundDurationnumberMin seconds in background before applying (ON_NEXT_RESUME)

AppSpacer.restartApp()

Reload the JavaScript bundle immediately.

AppSpacer.restartApp();

AppSpacer.notifyAppReady()

Mark the current update as successfully running. Called automatically by init(), but you can call it explicitly after your app finishes loading critical screens.

AppSpacer.notifyAppReady();

Important: If this method is never called after an update, the SDK assumes the update crashed and will roll back on the next boot.


AppSpacer.getStatus()

Returns the current UpdateStatus synchronously.

const status = AppSpacer.getStatus(); // e.g. UpdateStatus.IDLE

AppSpacer.onStatusChange(listener)

Subscribe to status changes. Returns an unsubscribe function.

const unsubscribe = AppSpacer.onStatusChange((status, detail) => { console.log('Status changed:', status, detail); }); // Later: unsubscribe();

AppSpacer.destroy()

Tear down auto-check timers and AppState listeners. Safe to call multiple times. Useful during development hot-reloads.

AppSpacer.destroy();

Types & Enums

AppSpacerConfig

interface AppSpacerConfig { apiUrl?: string; // Backend URL (default: https://api.appspacer.com/api) deploymentKey: string; // Deployment key (e.g. "sk_xxxx") appVersion: string; // Native app version (e.g. "1.0.0") checkInterval?: number; // Auto-check interval in seconds (default: 0) }

AppSpacerUpdate

interface AppSpacerUpdate { updateAvailable: boolean; // Whether an update is available releaseId?: string; // Server-assigned release ID packageUrl?: string; // Download URL for the zip hash?: string; // SHA-256 hash for verification mandatory?: boolean; // Whether this is a mandatory update description?: string; // Release description label?: string; // Release label (e.g. "v1", "v2") packageSize?: number; // Package size in bytes }

UpdateStatus

enum UpdateStatus { IDLE // No update activity CHECKING // Checking the server UPDATE_AVAILABLE // An update is available DOWNLOADING // Downloading the bundle INSTALLING // Verifying and installing INSTALLED // Ready, awaiting restart UP_TO_DATE // Already on latest version ERROR // Something went wrong }

InstallMode

enum InstallMode { IMMEDIATE // Restart immediately after install ON_NEXT_RESTART // Apply on next cold start ON_NEXT_RESUME // Apply when app returns from background }

UpdateUIType

enum UpdateUIType { PREMIUM_DARK // Sleek dark mode with glow effects (default) GLASS // Modern glassmorphism MINIMAL // Clean minimalist light theme BOTTOM_SHEET // iOS-inspired slide-up sheet FULL_SCREEN // Immersive full-screen experience }

UpdateDialogOptions

interface UpdateDialogOptions { title?: string; // Default: "Update Available" message?: string; // Default: "An update is available..." installButtonLabel?: string; // Default: "Install" ignoreButtonLabel?: string; // Default: "Ignore" mandatoryContinueButtonLabel?: string; // Default: "Continue" mandatoryUpdateMessage?: string; // Default: "An update is available that must be installed." appendReleaseDescription?: boolean; // Default: true }

Update Lifecycle

App Boots AppSpacer.init() ← calls notifyAppReady() automatically IDLE → CHECKING → (no update) → UP_TO_DATE → (update found) → UPDATE_AVAILABLE DOWNLOADING INSTALLING INSTALLED ┌──────────┼──────────────┐ ↓ ↓ ↓ IMMEDIATE ON_NEXT_RESTART ON_NEXT_RESUME (restart (waits for (waits for app now) cold start) to resume) ↓ ↓ ↓ App reloads with new bundle notifyAppReady() ← confirms update is stable

If any step fails, the status transitions to ERROR.


Safety & Rollback

Automatic Crash Rollback

The SDK protects your users from broken updates with a multi-layer safety system:

  1. When an update is installed, the SDK sets a PENDING flag
  2. On next boot, init() calls notifyAppReady(), which flips the flag to SUCCESS
  3. If the app crashes before notifyAppReady() runs, the native module detects the stale PENDING state on next boot and:
    • Deletes the broken bundle
    • Restores the previous stable bundle
    • Falls back to the binary-embedded bundle if no previous version exists

SHA-256 Verification

Every downloaded package is verified against the server-provided SHA-256 hash before extraction. If the hash doesn’t match, the update is rejected and the download is discarded.

Atomic Install

The update process uses an atomic swap pattern to prevent partial installs:

  1. current_bundle/ → renamed to previous_bundle/
  2. New update → extracted and renamed to current_bundle/

This ensures the app always has a valid bundle to load.

Hermes Compatibility

The SDK is fully compatible with Hermes engine. It uses a hash-routing system where new updates are extracted to immutable update_[hash] directories. SharedPreferences updates a pointer — this completely bypasses Hermes MMAP file locks.


Troubleshooting

IssueCauseSolution
Native module not foundNative module not linkedRun pod install (iOS) and rebuild the app
Update not detectedVersion mismatchEnsure appVersion in init() matches the target_version of your release
Bundle not loading after restartNative setup incompleteVerify bundleURL (iOS) and getJSBundleFile (Android) are overridden — or re-run npx appspacer setup
FORBIDDEN error on CLIAuth issueVerify you’re a member of the app’s organization
Network timeoutServer unreachableDefault timeout is 15s — check server health and connectivity
Update keeps rolling backCrash before notifyAppReadyEnsure AppSpacer.init() runs early in app startup, before any crash-prone code
Stale bundle after updateOld cached bundleClear app data or reinstall to reset OTA state
Update doesn’t restartNative setup missingRun npx appspacer setup or manually update MainApplication.kt
Assets missingAssets not included in bundleUse --include-assets flag when pushing updates

FAQ

Q: What happens if the user has no internet? A: checkForUpdate() will throw an error, status becomes ERROR. The app continues running the current bundle normally.

Q: Can I update native code (Java / Kotlin / Obj-C / Swift)? A: No. OTA updates only apply to the JavaScript bundle and assets. Native code changes require a full app store release.

Q: How big can an update be? A: There’s no hard limit from the SDK, but keep bundles small (< 20 MB) for the best user experience. The SDK streams the download to disk.

Q: Is the update delta or full? A: Currently full-bundle only. Delta updates are on the roadmap.

Q: Can I roll back a bad update from the server? A: Yes — use appspacer rollback -a <app> -d <deployment> -p <platform>. The SDK also auto-rolls back on crash.

Q: Does the SDK work with Hermes? A: Yes. Bundle the Hermes bytecode (.hbc) as you normally would, zip it, and push via CLI.

Q: What about Expo? A: Not currently supported. The SDK requires native module linking which is not available in Expo Go. It may work with a custom dev client (bare workflow).

Q: Is appspacer setup safe to run multiple times? A: Yes. It’s idempotent — it detects existing injections using // APPSPACER_START / // APPSPACER_END markers and replaces them cleanly.

Last updated on