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 setupIn 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 Found | Detected Architecture |
|---|---|
ReactHostDelegate, ReactHostImpl, getReactHost, DefaultReactHost | New Architecture |
ReactNativeHost, getReactNativeHost, getJSBundleFile | Traditional |
DefaultNewArchitectureEntryPoint | New 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_ENDThese 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
- Reversible —
appspacer setup:undoremoves 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:
- Adds
#import <AppSpacerModule.h>(for Obj-C) if not present - Replaces the body of
bundleURLorsourceURLForBridge:to return[AppSpacerModule bundleURL] - If neither method exists, injects a new
bundleURLmethod 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.bakBackups 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-runThis previews the exact code that would be injected without modifying any files.
Undoing Everything
Changed your mind? One command reverses all changes:
appspacer setup:undoThis 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-iosRead the full CLI reference and SDK guide for next steps.