Skip to Content
BlogZero-Config Native Setup: How appspacer setup Works Under the Hood

Zero-Config Native Setup: How appspacer setup Works Under the Hood

April 3, 2026


The hardest part of integrating any OTA update library into a React Native project isn’t the JavaScript side — it’s the native configuration. You need to override how Android and iOS load the JavaScript bundle, and the exact code depends on:

  • Whether you’re using New Architecture (ReactHostDelegate / ReactHostImpl) or Traditional Architecture (ReactNativeHost)
  • Whether your iOS project uses Objective-C (AppDelegate.mm) or Swift (AppDelegate.swift)
  • Which React Native version you’re targeting
  • Your specific project structure (monorepo, custom directory layouts, etc.)

Getting this wrong means your app either ignores OTA updates or — worse — crashes on startup.

AppSpacer solves this with a single command:

npx appspacer setup

In this post, we’ll walk through exactly what this command does, how it detects your project configuration, and the engineering decisions behind it.


The Problem: A Matrix of Native Configurations

React Native has evolved significantly. A project created with RN 0.68 looks very different from one on RN 0.76+. Here’s what the bundle loading code looks like for each scenario:

Android — Traditional Architecture (RN < 0.73)

override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { override fun getJSBundleFile(): String? = com.appspacer.AppSpacerModule.getCustomBundlePath(applicationContext) }

Android — New Architecture (RN 0.73+)

override val reactHost: ReactHost by lazy { val delegate = object : ReactHostDelegate { override fun getJsBundleFilePath(): String? = com.appspacer.AppSpacerModule.getCustomBundlePath(applicationContext) } ReactHostImpl(applicationContext, delegate, SurfaceDelegateFactory { null }, true, true) }

iOS — Objective-C

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

iOS — Swift

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

That’s four different code snippets across two platforms, and each needs to be injected into the correct location within the correct file. Miss one, and OTA updates silently fail. Get the architecture wrong, and your app crashes.

No developer should have to deal with this manually.


How appspacer setup Works

The setup command follows a methodical pipeline:

Step 1: SDK Verification

Before touching any native files, the CLI checks that react-native-appspacer is installed:

node_modules/react-native-appspacer/ — exists? ✓

If it’s not installed, the command exits with a helpful message telling you to run npm install react-native-appspacer first. No point configuring native files for a package that doesn’t exist.

Step 2: Android Project Discovery

The CLI searches for MainApplication.kt (or .java) by recursively scanning:

android/app/src/main/java/** android/app/src/main/kotlin/**

This handles standard projects, custom package names, and nested directory structures. The file is found regardless of your package name or project layout.

Step 3: Architecture Detection

This is the interesting part. The CLI reads the content of MainApplication.kt and uses pattern matching to determine which architecture you’re using:

Pattern FoundDetected Architecture
ReactHostDelegate, ReactHostImpl, getReactHost, DefaultReactHostNew Architecture
ReactNativeHost, getReactNativeHost, getJSBundleFileTraditional
DefaultNewArchitectureEntryPointNew Architecture

The detection is conservative — if no New Architecture patterns are found, it defaults to Traditional, which is the safer choice.

Step 4: Code Injection

Based on the detected architecture, the CLI generates the correct Kotlin code and injects it using AST-aware pattern matching (not simple string replacement). The injection targets specific code structures:

For Traditional Architecture: The CLI looks for the object : DefaultReactNativeHost block and injects the getJSBundleFile() override inside it.

For New Architecture: The CLI looks for the ReactHostDelegate object expression and injects getJsBundleFilePath() inside it.

Fallback: If neither pattern matches cleanly (custom project layouts, refactored code), the CLI falls back to injecting at the class body level — still correct, just less surgically targeted.

All injected code is wrapped in markers:

// APPSPACER_START // AppSpacer OTA — Dynamic bundle resolution (New Architecture) override fun getJsBundleFilePath(): String? = com.appspacer.AppSpacerModule.getCustomBundlePath(applicationContext) // APPSPACER_END

These markers make the injection:

  • Identifiable — the CLI can detect if setup has already been run
  • Replaceable — re-running setup cleanly removes the old injection and applies fresh code
  • Reversibleappspacer setup:undo removes everything between the markers

Step 5: iOS Configuration

The same process runs for iOS. The CLI searches for AppDelegate.mm, AppDelegate.m, or AppDelegate.swift inside the ios/ directory (including subdirectories), then:

  1. Adds #import <AppSpacerModule.h> (for Obj-C) if not present
  2. Replaces the body of bundleURL or sourceURLForBridge: to return [AppSpacerModule bundleURL]
  3. If neither method exists, injects a new bundleURL method before @end

Step 6: Backup & Safety

Before writing any changes, the CLI creates .appspacer.bak backup files:

MainApplication.kt → MainApplication.kt.appspacer.bak AppDelegate.mm → AppDelegate.mm.appspacer.bak

Backups are only created once (preserving the original). If you need to restore, just rename the backup file.


Edge Cases We Handle

DefaultReactHost Pattern

Some RN 0.76+ projects use a shorthand DefaultReactHost.getDefaultReactHost(...) that cannot be safely auto-patched. In this case, the CLI injects a comment block with manual instructions and prints a warning:

⚠ DefaultReactHost pattern detected — manual action required. A comment block with instructions has been injected.

We chose this approach over risky auto-patching because silently breaking someone’s app is worse than asking them to make one manual change.

Already Configured

Running appspacer setup twice is safe. The CLI detects existing APPSPACER_START / APPSPACER_END markers, removes the old injection, and applies a fresh one. This is useful when upgrading React Native versions — the correct architecture-specific code is re-generated.

Dry Run

Not sure what will change? Use --dry-run:

appspacer setup --dry-run

This previews the exact code that would be injected without modifying any files.


Undoing Everything

Changed your mind? One command reverses all changes:

appspacer setup:undo

This scans both Android and iOS native files, removes everything between the APPSPACER_START / APPSPACER_END markers, and leaves your project exactly as it was.


Why This Matters

Most OTA update libraries ship a README with 4 different code snippets and say “pick the one that matches your setup.” That’s fine for experienced teams, but it’s a minefield for everyone else:

  • Wrong architecture detection → app crashes on startup
  • Missing import statement → native module not found
  • Injected in wrong location → OTA updates silently ignored
  • No rollback mechanism → manual git revert when things go wrong

appspacer setup eliminates all of these failure modes. One command, automatic detection, correct injection, backup files, and a clean undo path.


Try It

If you’re starting a new AppSpacer integration, the setup flow is:

# Install the SDK npm install react-native-appspacer cd ios && pod install && cd .. # Auto-configure native files npx appspacer setup # Rebuild npx react-native run-android npx react-native run-ios

Read the full CLI reference and SDK guide for next steps.

Last updated on