pulse-updates
v1.0.9
Published
OTA updates for React Native - lightweight alternative to expo-updates
Maintainers
Readme
Pulse Updates
Lightweight OTA (Over-The-Air) updates for React Native apps. A simpler alternative to expo-updates with full support for React Native's New Architecture (Fabric/Bridgeless).
Features
- Full support for React Native New Architecture (Bridgeless mode)
- Automatic asset resolution with embedded fallback
- Hermes bytecode compilation for faster startup
- Incremental updates (only changed assets are downloaded)
- Rollback protection with health checks
- Channel-based deployments (production, staging, etc.)
- Compatible with expo-asset resolution
Installation
npm install pulse-updates
# or
yarn add pulse-updatesiOS Setup
Add to your Podfile:
pod 'pulse-updates', :path => '../node_modules/pulse-updates'Then run:
cd ios && pod installAdd these keys to your Info.plist:
<key>PulseUpdatesEnabled</key>
<true/>
<key>PulseUpdatesURL</key>
<string>https://your-update-server.com</string>
<key>PulseUpdatesRuntimeVersion</key>
<string>$(MARKETING_VERSION)</string>
<key>PulseUpdatesCheckOnLaunch</key>
<string>ALWAYS</string>Android Setup
Add to your android/app/build.gradle:
dependencies {
implementation project(':pulse-updates')
}Add metadata to AndroidManifest.xml inside <application>:
<meta-data android:name="PulseUpdatesEnabled" android:value="true" />
<meta-data android:name="PulseUpdatesURL" android:value="https://your-update-server.com" />
<meta-data android:name="PulseUpdatesRuntimeVersion" android:value="@string/app_version" />
<meta-data android:name="PulseUpdatesCheckOnLaunch" android:value="ALWAYS" />New Architecture (Bridgeless) Setup
For React Native 0.76+ with New Architecture enabled, update your MainApplication.kt:
import app.pulse.updates.PulseUpdatesModule
import app.pulse.updates.PulseReactHostFactory
class MainApplication : Application(), ReactApplication {
// Extract packages into a reusable method
private fun buildPackages(): List<ReactPackage> {
val packages = PackageList(this).packages.toMutableList()
// Add your custom packages here
return packages
}
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> = buildPackages()
override fun getJSMainModuleName(): String = "index"
// Use Pulse Updates bundle
override fun getJSBundleFile(): String? {
return PulseUpdatesModule.getBundleFile(applicationContext)?.absolutePath
?: super.getJSBundleFile()
}
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
// For New Architecture: use PulseReactHostFactory
override val reactHost: ReactHost
get() = PulseReactHostFactory.createReactHost(
applicationContext,
packages = buildPackages(),
jsMainModuleName = "index",
useDevSupport = BuildConfig.DEBUG
)
}Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| PulseUpdatesEnabled | boolean | true | Enable/disable OTA updates |
| PulseUpdatesURL | string | required | Your update server URL |
| PulseUpdatesRuntimeVersion | string | app version | Version for update compatibility |
| PulseUpdatesCheckOnLaunch | string | ALWAYS | When to check: ALWAYS, WIFI_ONLY, NEVER |
| PulseUpdatesChannel | string | production | Update channel |
| PulseUpdatesLaunchWaitMs | number | 0 | Wait time for update check on launch |
JavaScript API
Basic Usage
import * as PulseUpdates from 'pulse-updates';
import { initializeAssetResolver } from 'pulse-updates';
// Initialize at app startup
async function initUpdates() {
// Refresh state from native module
await PulseUpdates.refreshStateAsync();
// Initialize asset resolver for images
initializeAssetResolver(PulseUpdates.localAssets);
// Check current state
console.log('Update ID:', PulseUpdates.updateId);
console.log('Is embedded:', PulseUpdates.isEmbeddedLaunch);
}
// Check for updates
async function checkUpdates() {
const result = await PulseUpdates.checkForUpdateAsync();
if (result.isAvailable) {
console.log('Update available:', result.manifest?.updateId);
// Download the update
const fetchResult = await PulseUpdates.fetchUpdateAsync();
if (fetchResult.isNew) {
// Reload to apply
await PulseUpdates.reloadAsync();
}
}
}React Hook
import { usePulseUpdates } from 'pulse-updates';
function UpdateBanner() {
const {
isChecking,
isDownloading,
availableUpdate,
downloadedUpdate,
checkForUpdate,
downloadUpdate,
reload
} = usePulseUpdates();
if (downloadedUpdate) {
return (
<View>
<Text>Update ready!</Text>
<Button title="Restart" onPress={reload} />
</View>
);
}
if (availableUpdate) {
return (
<View>
<Text>Update available</Text>
<Button
title={isDownloading ? "Downloading..." : "Download"}
onPress={downloadUpdate}
disabled={isDownloading}
/>
</View>
);
}
return null;
}API Reference
State Properties
PulseUpdates.isEnabled // boolean - Updates enabled
PulseUpdates.updateId // string | null - Current update ID
PulseUpdates.runtimeVersion // string | null - Runtime version
PulseUpdates.channel // string | null - Update channel
PulseUpdates.isEmbeddedLaunch // boolean - Running embedded bundle
PulseUpdates.manifest // PulseManifest | null - Current manifest
PulseUpdates.localAssets // Record<string, string> | null - Local asset mapMethods
// Refresh state from native
await PulseUpdates.refreshStateAsync()
// Check for available updates
const result = await PulseUpdates.checkForUpdateAsync()
// Returns: { isAvailable: boolean, manifest?: PulseManifest }
// Download available update
const result = await PulseUpdates.fetchUpdateAsync()
// Returns: { isNew: boolean, manifest?: PulseManifest }
// Reload app with new update
await PulseUpdates.reloadAsync()
// Mark app as successfully launched (for rollback protection)
await PulseUpdates.markAppReady()
// Report launch failure (triggers rollback)
await PulseUpdates.reportLaunchFailure(reason: string)CLI Commands
Publishing Updates
# Publish an update
npx pulse-updates publish --platform <ios|android> --build <number>
# With all options
npx pulse-updates publish \
--platform ios \
--build 42 \
--channel production \
--api-key your-api-key \
--api-url https://your-server.com \
--runtime-version 1.0.0 \
--message "Bug fixes and improvements"CLI Options
| Option | Description |
|--------|-------------|
| --platform | Target platform: ios or android (required) |
| --build | Build number for this update (required) |
| --channel | Update channel (default: production) |
| --api-key | Server API key (or set in pulse.config.json) |
| --api-url | Server URL (auto-detected from native config) |
| --runtime-version | Runtime version (auto-detected from native config) |
| --message | Release notes |
| --skip-bundle | Skip bundle creation (use existing) |
Configuration File
Create pulse.config.json in your project root:
{
"apiKey": "your-api-key",
"apiUrl": "https://your-server.com",
"channel": "production"
}Server Requirements
Pulse Updates requires a compatible server. The server must implement:
POST /api/releases- Create new releasePOST /api/assets/check- Check which assets existPOST /api/assets/upload- Upload assetsPOST /api/releases/:id/finalize- Finalize releaseGET /api/manifest/:appId- Get latest manifest
Embedded Manifest
For offline-first support, generate an embedded manifest during your build:
node node_modules/pulse-updates/scripts/generate-embedded-manifest.mjs \
--bundle path/to/index.bundle \
--assets path/to/assets \
--out path/to/output \
--platform ios \
--runtime-version 1.0.0Troubleshooting
Images not loading after update
Ensure you initialize the asset resolver at app startup:
import { initializeAssetResolver } from 'pulse-updates';
import * as PulseUpdates from 'pulse-updates';
await PulseUpdates.refreshStateAsync();
initializeAssetResolver(PulseUpdates.localAssets);Update not applying on reload
For New Architecture apps, ensure you're using PulseReactHostFactory in your MainApplication.kt.
Debug Logging
Set PULSE_DEBUG=true environment variable to enable verbose logging in native code.
License
MIT
