open-expo-ota
v0.1.18
Published
Client library for OpenExpoOTA - A self-hosted OTA update system for Expo apps
Maintainers
Readme
OpenExpoOTA Client
React Native client library for OpenExpoOTA - A self-hosted OTA update system for Expo apps.
How It Works
The OpenExpoOTA client works as a layer on top of Expo's built-in expo-updates package. Here's the flow:
- Check for Updates: Your app queries your OpenExpoOTA backend server to see if a new update is available
- Download Update: If an update is found, the client uses
expo-updates.fetchUpdateAsync()to download the new bundle - Apply Update: The client uses
expo-updates.reloadAsync()to restart the app with the new update
The key advantage is that you control the server, the update distribution, and can customize the entire update experience.
Installation
npm install openexpoota-client expo-updatesQuick Setup
1. Initialize Your App with the CLI
First, set up your app in the OpenExpoOTA backend:
# In your Expo project directory
npx openexpoota-cli login
npx openexpoota-cli initThis will create an ota.config.json file with your app configuration.
2. Configure app.json
Update your app.json to work with OpenExpoOTA:
{
"expo": {
"name": "your-app",
"slug": "your-app-slug-from-ota-config",
"version": "1.0.0",
"runtimeVersion": "1.0.0",
"updates": {
"url": "http://your-backend-url.com/api/manifest/your-app-slug-from-ota-config?channel=production&runtimeVersion=1.0.0",
"enabled": false,
"checkAutomatically": "NEVER",
"fallbackToCacheTimeout": 0,
"disableAntiBrickingMeasures": true
},
"ios": {
"runtimeVersion": "1.0.0"
},
"android": {
"runtimeVersion": "1.0.0"
}
}
}Important Notes:
- The
slugmust match the slug from yourota.config.json - Set
updates.enabled: falseto disable Expo's automatic checking - Set
checkAutomatically: "NEVER"to prevent conflicts - Add
disableAntiBrickingMeasures: truefor development - Use the same
runtimeVersionacross all platforms
3. Add the Provider to Your App
import React from 'react';
import { UpdatesProvider } from 'openexpoota-client';
import { ReleaseChannel } from 'openexpoota-client';
export default function App() {
return (
<UpdatesProvider
config={{
backendUrl: 'http://your-backend-url.com/api',
appSlug: 'your-app-slug-from-ota-config',
channel: ReleaseChannel.PRODUCTION,
runtimeVersion: '1.0.0',
checkOnLaunch: true,
autoInstall: false, // Set to true for automatic updates
debug: true // Enable for development
}}
>
<YourMainApp />
</UpdatesProvider>
);
}4. Use Updates in Components
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useSelfHostedUpdates } from 'openexpoota-client';
function UpdateScreen() {
const {
isChecking,
isUpdateAvailable,
updateInfo,
checkForUpdates,
downloadUpdate,
applyUpdate,
error,
progress
} = useSelfHostedUpdates();
if (error) {
return <Text>Error: {error.message}</Text>;
}
if (isChecking) {
return <Text>Checking for updates...</Text>;
}
if (isUpdateAvailable) {
return (
<View>
<Text>Update Available: {updateInfo?.version}</Text>
{progress ? (
<Text>Downloading: {Math.round(progress * 100)}%</Text>
) : (
<Button
title="Download & Install"
onPress={async () => {
await downloadUpdate();
applyUpdate();
}}
/>
)}
</View>
);
}
return (
<View>
<Text>Your app is up to date!</Text>
<Button title="Check for Updates" onPress={checkForUpdates} />
</View>
);
}Configuration Options
UpdatesProvider Config
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| backendUrl | string | Yes | - | URL of your OpenExpoOTA backend API |
| appSlug | string | Yes | - | App identifier (from ota.config.json) |
| channel | ReleaseChannel | No | PRODUCTION | Release channel to use |
| runtimeVersion | string | No | From Constants | Runtime version for compatibility |
| checkOnLaunch | boolean | No | true | Check for updates when app starts |
| autoInstall | boolean | No | false | Automatically download and install updates |
| debug | boolean | No | false | Enable debug logging |
Release Channels
enum ReleaseChannel {
PRODUCTION = 'production',
STAGING = 'staging',
DEVELOPMENT = 'development'
}Advanced Features
Custom Update Flow
function AdvancedUpdateFlow() {
const updates = useSelfHostedUpdates();
const [updateState, setUpdateState] = useState('idle');
const handleUpdate = async () => {
try {
setUpdateState('checking');
await updates.checkForUpdates();
if (updates.isUpdateAvailable) {
setUpdateState('downloading');
await updates.downloadUpdate();
setUpdateState('installing');
updates.applyUpdate(); // This restarts the app
}
} catch (error) {
setUpdateState('error');
console.error('Update failed:', error);
}
};
return (
<View>
<Text>Update State: {updateState}</Text>
<Button title="Check & Update" onPress={handleUpdate} />
</View>
);
}Progress Tracking
function UpdateWithProgress() {
const { progress, downloadUpdate, isUpdateAvailable } = useSelfHostedUpdates();
return (
<View>
{isUpdateAvailable && (
<View>
<Button title="Download Update" onPress={downloadUpdate} />
{progress && (
<View style={{ marginTop: 10 }}>
<Text>Progress: {Math.round(progress * 100)}%</Text>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{ width: `${progress * 100}%` }
]}
/>
</View>
</View>
)}
</View>
)}
</View>
);
}Event Listening
import SelfHostedUpdates from 'openexpoota-client/src/updates';
// Direct usage of the updates class
const updates = new SelfHostedUpdates({
backendUrl: 'http://your-backend.com/api',
appSlug: 'your-app-slug',
channel: ReleaseChannel.PRODUCTION,
debug: true
});
// Listen to all update events
updates.addEventListener((event) => {
switch (event.type) {
case 'checking':
console.log('Checking for updates...');
break;
case 'updateAvailable':
console.log('Update available:', event.manifest);
break;
case 'updateNotAvailable':
console.log('No update available');
break;
case 'downloadStarted':
console.log('Download started');
break;
case 'downloadFinished':
console.log('Download finished');
break;
case 'installed':
console.log('Update installed');
break;
case 'error':
console.error('Update error:', event.error);
break;
}
});Publishing Updates
Use the OpenExpoOTA CLI to publish updates:
# Publish to development channel
ota publish --channel development
# Publish to production with specific version
ota publish --channel production --version 1.2.0
# Publish with runtime version compatibility
ota publish --channel production --target-version-range ">=1.0.0 <2.0.0"Runtime Version Compatibility
The client automatically handles runtime version compatibility:
- Exact Match: Update with
runtimeVersion: "1.0.0"only applies to apps with runtime version1.0.0 - Range Match: Update with
targetVersionRange: ">=1.0.0 <2.0.0"applies to compatible app versions - Platform Specific: Updates can target specific platforms (iOS, Android)
App Configuration Requirements
Required app.json Settings
{
"expo": {
"slug": "must-match-ota-config-slug",
"runtimeVersion": "1.0.0",
"updates": {
"url": "http://backend.com/api/manifest/your-slug?channel=production&runtimeVersion=1.0.0",
"enabled": false,
"checkAutomatically": "NEVER",
"disableAntiBrickingMeasures": true
},
"ios": {
"runtimeVersion": "1.0.0"
},
"android": {
"runtimeVersion": "1.0.0"
}
}
}Required ota.config.json
This file is created by ota init:
{
"appId": 123,
"slug": "your-app-slug",
"backendUrl": "http://your-backend.com/api"
}Building for Production
Development Builds
For development and testing:
npx expo run:ios --configuration Release
npx expo run:android --configuration ReleaseProduction Builds
For production releases:
# Build for app stores
eas build --platform all --profile production
# Or local builds
npx expo run:ios --configuration Release
npx expo run:android --configuration ReleaseTroubleshooting
Common Issues
1. "Cannot find module 'expo-updates'"
Solution: Install expo-updates
npm install expo-updates2. Updates not working in development
Problem: Expo Go doesn't support custom updates
Solution: Use development builds (expo run:ios / expo run:android)
3. Runtime version mismatch
Problem: App and update have incompatible runtime versions Solution: Ensure consistent runtime versions in:
app.json→runtimeVersionapp.json→ios.runtimeVersionapp.json→android.runtimeVersion- Client config →
runtimeVersion
4. 400 errors when checking for updates
Problem: Usually missing or incorrect query parameters
Solution: Check that app.json updates.url includes correct parameters:
http://backend.com/api/manifest/your-slug?channel=production&runtimeVersion=1.0.05. Download fails with fetch errors
Problem: expo-updates using wrong configuration Solution: Ensure:
updates.enabled: falsein app.jsoncheckAutomatically: "NEVER"in app.json- Correct slug in app.json matches ota.config.json
Debug Mode
Enable debug logging to troubleshoot:
<UpdatesProvider
config={{
// ... other config
debug: true
}}
>This will log detailed information about:
- API requests and responses
- Runtime version comparisons
- Update availability decisions
- Download progress
- Error details
Network Requirements
- Your backend API must be accessible from the device
- For iOS simulator/Android emulator: use
http://localhost:3000 - For physical devices: use your computer's IP address
http://192.168.1.100:3000 - For production: use HTTPS
https://your-domain.com
Security Considerations
- The system uses app-level authentication (no user credentials on device)
- App slugs and channels provide access control
- Use HTTPS in production
- Runtime version compatibility prevents incompatible updates
- Updates are cryptographically verified by expo-updates
Compatibility
- Expo SDK: 49+ (with expo-updates)
- React Native: 0.72+
- Platforms: iOS, Android
- Build Types: Development builds, Production builds (not Expo Go)
License
MIT
Important Notes
⚠️ App Restart Required: When the OpenExpoOTA client configures expo-updates at startup with custom URLs, the configuration only takes effect on the next app launch. If you change your configuration (like app slug, channel, or backend URL), users will need to completely close and reopen the app.
⚠️ Release Builds Only: Most update functionality only works in release builds. Development builds using Expo Go will not fully support OTA updates.
⚠️ Network Requirements: Update checking and downloading requires an active internet connection.
