Install and connect native
Follow the path for React Native, Expo or HarmonyOS and prepare a Release build that can load updates.
Updated 2026-09-15
On this page
- Prepare your environment
- Install the SDK
- Sign in and create or select an application
- Create an application
- Select an existing application
- `appId`, `appKey` and `update.json`
- Android: connect bundle loading
- Java projects
- iOS: use Pakta for Release bundles
- Swift and older AppDelegate templates
- Expo: build with native modules
- HarmonyOS: wire the RNOH integration
- HarmonyOS file changes
- Self-hosting: configure SDK and CLI separately
- Manual linking: only when autolinking is unavailable
- Android activity restoration with react-native-screens
- Optional: open a test update with a link
- Android AAB installers
- Next step
Prepare your environment
A React Native, Expo or RNOH project that already builds. Commands run from the app root containing
package.jsonunless stated otherwise.Node.js ≥18.17 and your existing platform toolchain; iOS builds require macOS.
A separate test application and device. Start with one platform.
SDK peer dependencies declare React ≥16.8 and RN ≥0.59; that is not a test guarantee for every version and architecture. Keep your project's version and follow its host structure below.
Install the SDK
Choose your project type. Run its commands in the directory containing package.json.
npm install -g rn-update-cli
npm install rn-updatenpm install -g rn-update-cli
npx expo install rn-updateFor React Native iOS projects, run pod install inside ios (or bundle exec pod install with Bundler), then return to the app root. Expo generates its native project during the build step; run npx expo prebuild first when the native directories do not exist.
Sign in and create or select an application
Register in the Pakta console, then sign in from the app root:
pakta loginBy default, the CLI stores the session as .pakta.token in the current directory and adds the CLI temporary directory .pakta to .gitignore when the project already has one. Confirm neither path is committed; never put passwords, session files or PAKTA_API_TOKEN in the repository. When RNU_SERVICE_URL points to a self-hosted service, credentials are stored separately per service URL.
Create an application
Android, iOS and HarmonyOS are separate platform applications. Create each one as needed. The CLI selects a newly created application and writes it to update.json:
Run only the lines for platforms you actually ship; do not create unused platform applications.
pakta createApp --platform android --name PaktaDemo
pakta createApp --platform ios --name PaktaDemo
pakta createApp --platform harmony --name PaktaDemoThe names may match; the platform must match the installer you build.
Select an existing application
If the application already exists in the console, list the platform's applications and select one:
pakta apps --platform android
pakta selectApp --platform androidThe CLI shows the application and its appId; enter the appId you want to use. Replace the platform with ios or harmony for those targets. A single update.json can hold all three selections; selecting one platform preserves the other entries.
`appId`, `appKey` and `update.json`
After selection, the app root contains a file like this:
{
"android": {
"appId": "YOUR_ANDROID_APP_ID",
"appKey": "YOUR_ANDROID_APP_KEY"
},
"ios": {
"appId": "YOUR_IOS_APP_ID",
"appKey": "YOUR_IOS_APP_KEY"
},
"harmony": {
"appId": "YOUR_HARMONY_APP_ID",
"appKey": "YOUR_HARMONY_APP_KEY"
}
}The real file contains only platforms you have created or selected. Legacy services may write numeric appId values; the Pakta standalone service may write UUIDs. Both are server-generated application identities. Do not edit, sort or convert them.
| Identifier | Sensitive? | Used for |
|---|---|---|
appId | No | CLI, management API and native package registration |
appKey | Public identifier | new Pakta({ appKey }) in the app |
.pakta.token | Yes | Local CLI session; keep it in .gitignore |
PAKTA_API_TOKEN | Yes | CI / CLI publishing; inject through Secrets only |
update.json contains no publishing credential and can be committed for team sharing. If application code imports it, its contents enter the JavaScript bundle; when only the CLI uses it, it remains publishing configuration. See root component setup for platform-specific appKey loading.
Follow only the section matching your project.
Android: connect bundle loading
If MainApplication.kt uses reactHost and getDefaultReactHost, add jsBundleFilePath to the existing host:
import cn.reactnative.modules.update.UpdateContext
// Keep existing ReactHost, PackageList and getDefaultReactHost imports.
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList = PackageList(this).packages,
jsBundleFilePath = UpdateContext.getBundleUrl(this),
)
}For DefaultReactNativeHost / ReactNativeHost, import UpdateContext and override the method in the existing host object:
override fun getJSBundleFile(): String? =
UpdateContext.getBundleUrl(this@MainApplication)Disable PNG crunching in the existing release configuration to reduce resource byte differences:
android {
buildTypes {
release {
crunchPngs false
}
}
}Autolinking connects the module and the SDK's Gradle integration provides build identity. Do not invent buildTime. Retain existing signing and build settings.
Connect the root component, then run ./gradlew assembleRelease from android, or .\gradlew.bat assembleRelease on Windows. Use the matching Release task for flavored projects.
Java projects
Open android/app/src/main/java/your/package/MainApplication.java. Add import cn.reactnative.modules.update.UpdateContext; at the top and this override inside the existing ReactNativeHost or DefaultReactNativeHost object:
@Override
protected String getJSBundleFile() {
return UpdateContext.getBundleUrl(MainApplication.this);
}iOS: use Pakta for Release bundles
Install Pods from ios: bundle exec pod install for Bundler projects, otherwise pod install. Choose one matching your RN template below: RN 0.74+ uses bundleURL; older templates usually use sourceURLForBridge:.
#import "RCTPakta.h"
- (NSURL *)bundleURL
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [RCTPakta bundleURL];
#endif
}#import "RCTPakta.h"
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [RCTPakta bundleURL];
#endif
}Keep existing Debug entry points, module names and lifecycle methods. In Swift templates, the actual Release bundle URL provider must return RCTPakta.bundleURL(). Expose RCTPakta.h through the project's Objective-C bridging header instead of replacing the entire AppDelegate.
After connecting the root, open .xcworkspace in Xcode, select signing and a device, and verify a Release build. Retain the IPA from the same archive for registration.
Swift and older AppDelegate templates
Add #import "RCTPakta.h" to your app target's Objective-C bridging header. If needed, create that header and set its relative path, such as YourApp/YourApp-Bridging-Header.h, in Build Settings → Objective-C Bridging Header.
Modify the existing URL provider. Newer templates may put this method inside ReactNativeDelegate:
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
return RCTPakta.bundleURL()
#endif
}For an older Objective-C template with only sourceURLForBridge:, retain that signature and return [RCTPakta bundleURL] in its non-DEBUG branch. Mixed native apps should obtain the URL through a bridge delegate rather than create the root view using a fixed bundle path.
Expo: build with native modules
Expo Go does not contain rn-update's native module. Follow these steps:
Install dependencies with
npx expo install rn-update. For a custom channel, add the built-in Config Plugin toexpo.plugins.Connect the root layout: wrap the existing
Stack/Slotinapp/_layout.tsxwithUpdateProvider; create the client outside components.Build the native app: run
npx expo run:android --variant release,npx expo run:ios --configuration Release, or your existing EAS production profile.Install and verify it starts without Metro. Later, create OTA packages using
bundle --expowith platformandroid/ios.
See the Expo guide for the complete steps.
HarmonyOS: wire the RNOH integration
HarmonyOS requires HAR, ArkTS and C++ integration beyond npm installation. Compare these locations with the example:
| Location | Connection |
|---|---|
harmony/entry/oh-package.json5 | Pakta HAR and RNOH dependencies, using your paths |
harmony/entry/hvigorfile.ts | Import SDK harmony/hvigor-plugin; add reactNativeUpdatePlugin() to plugins |
RNPackagesFactory.ets | Import PaktaPackage and add it to the RNPackage list |
PackageProvider.cpp / CMake | Register the Pakta C++ package and native linkage |
pages/Index.ets | Add PaktaFileJSBundleProvider, retaining embedded fallback |
pakta bundle --platform harmony --output .pakta/output/harmony.ppk --no-interactiveSync, sign and build the native .app in DevEco Studio. Confirm generated rawfile paths match the host, install and verify, then register with uploadApp.
Complete projects are in the SDK examples: testHotUpdate, expoUsePakta and harmony_use_pakta. Replace repository-relative paths with your dependency paths.
HarmonyOS file changes
These paths assume harmony and node_modules are siblings in the app root. Merge entries into existing dependencies, plugins and package lists. Retain all existing registrations. Add the HAR, Hvigor plugin, ArkTS and C++ registrations, then set the Release bundle provider:
"pakta": "file:../../node_modules/rn-update/harmony/pakta.har"import { hapTasks } from '@ohos/hvigor-ohos-plugin';
import { reactNativeUpdatePlugin } from '../../node_modules/rn-update/harmony/hvigor-plugin';
export default {
system: hapTasks,
plugins: [reactNativeUpdatePlugin()],
};import type { RNPackageContext, RNPackage } from '@rnoh/react-native-openharmony';
import PaktaPackage from 'pakta';
export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
return [new PaktaPackage(ctx)];
}set(PAKTA_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_modules/rn-update/harmony/pakta/src/main/cpp")
target_include_directories(rnoh_app PRIVATE "${PAKTA_CPP_DIR}")
target_sources(rnoh_app PRIVATE "${PAKTA_CPP_DIR}/PaktaTurboModule.cpp")#include "RNOH/PackageProvider.h"
#include "PaktaPackage.h"
using namespace rnoh;
std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) {
return {std::make_shared<PaktaPackage>(ctx)};
}import { PaktaFileJSBundleProvider } from 'pakta';
import { AnyJSBundleProvider, ResourceJSBundleProvider } from '@rnoh/react-native-openharmony';jsBundleProvider: new AnyJSBundleProvider([
new PaktaFileJSBundleProvider(this.rnohCoreContext.uiAbilityContext),
new ResourceJSBundleProvider(
this.rnohCoreContext.uiAbilityContext.resourceManager,
'bundle.harmony.js'
),
]),Keep the other RNApp properties and ensure rnohCoreContext is ready. Release builds should try the Pakta file before the embedded resource, with no Metro provider ahead of it.
See the complete Expo guide for its root layout and channel plugin.
Self-hosting: configure SDK and CLI separately
Hosted-service users can skip this section. The SDK uses the public update API; the CLI uses management endpoints:
server: {
main: ['https://YOUR_HOST/api'],
queryUrls: ['https://YOUR_CDN/endpoints.json'],
},Replace hosts with your deployment addresses. The discovery file must belong to your deployment. Set the CLI host before signing in:
$env:RNU_SERVICE_URL = 'https://YOUR_HOST'export RNU_SERVICE_URL=https://YOUR_HOSTManual linking: only when autolinking is unavailable
React Native 0.60+ normally autolinks. Run npx react-native config to check rn-update discovery and install iOS Pods. Skip manual registration if already linked.
Older Android projects need both Gradle dependency entries and React Package registration:
include ':rn-update'
project(':rn-update').projectDir = new File(rootProject.projectDir, '../node_modules/rn-update/android')implementation project(':rn-update')In MainApplication.java, add new UpdatePackage() to the existing getPackages() list and import it:
import cn.reactnative.modules.update.UpdatePackage;
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new UpdatePackage()
);
}Keep every existing package and append only new UpdatePackage(); do not create a second React Host. After linking, still configure UpdateContext.getBundleUrl(...) as described in Android bundle loading.
If iOS discovery fails, add pod 'rn-update', :path => '../node_modules/rn-update' inside the app target in Podfile and run pod install. Linking alone does not configure the bundle loading entry shown above.
Pakta does not ship a standalone RCTPakta.xcodeproj for direct insertion into an old Xcode project. For RN <0.60 without CocoaPods, enable CocoaPods or upgrade React Native first. A true manual integration must include ios/RCTPakta, the shared C++ core, the SSZipArchive dependency and pakta_build_time.txt in the app target, followed by Release startup and native-package registration checks.
Android activity restoration with react-native-screens
For apps using react-native-screens, check MainActivity restoration configuration. The following requires a version that supplies RNScreensFragmentFactory. Place the override directly in MainActivity, not MainActivityDelegate:
import android.os.Bundle
import com.swmansion.rnscreens.fragment.restoration.RNScreensFragmentFactory
override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = RNScreensFragmentFactory()
super.onCreate(savedInstanceState)
}Merge into an existing onCreate instead of adding a duplicate. Older releases without this class need their own screens installation instructions.
Optional: open a test update with a link
An existing scanner can call parseTestQrCode. To open the app from a browser or camera, configure your own scheme such as paktademo.
Add a separate filter inside the existing Android MainActivity activity, keep its launcher filter, and use android:launchMode="singleTask":
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="paktademo" />
</intent-filter>For iOS add paktademo in the app target's Info → URL Types and retain the app's React Native Linking forwarding in AppDelegate. Expo uses expo.scheme. Rebuild the native installer.
After uploading a test update, copy its full hash and open paktademo://update?type=__rnPaktaVersionHash&data=ACTUAL_HASH. The Provider checks it and follows your app's download/activation flow. testChannel:false rejects test payloads.
Android AAB installers
Retain the AAB actually submitted to Google Play. Inspect with pakta parseAab PATH, then register with pakta uploadAab PATH. Do not substitute an APK from a different build.
Do not disable every language, density and ABI split just to integrate. Pakta CLI extracts the base module by default; if the distributed build depends on additional splits, select them explicitly:
pakta uploadAab ./android/app/build/outputs/bundle/release/app-release.aab --splits config.xxhdpi,config.arm64_v8aYou can also use --includeAllSplits to create a registration APK containing every split. Verify resources and updates using the distributed build. Investigate resource packaging and the native version when a split lacks an asset.
Next step
Dependencies are installed, the CLI has selected the correct platform application, and native loading is configured. Connect your app, add the version panel, then build the native package.
Local Expo build flags follow the official Expo CLI reference.
