npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

open-expo-ota

v0.1.18

Published

Client library for OpenExpoOTA - A self-hosted OTA update system for Expo apps

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:

  1. Check for Updates: Your app queries your OpenExpoOTA backend server to see if a new update is available
  2. Download Update: If an update is found, the client uses expo-updates.fetchUpdateAsync() to download the new bundle
  3. 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-updates

Quick 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 init

This 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 slug must match the slug from your ota.config.json
  • Set updates.enabled: false to disable Expo's automatic checking
  • Set checkAutomatically: "NEVER" to prevent conflicts
  • Add disableAntiBrickingMeasures: true for development
  • Use the same runtimeVersion across 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 version 1.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 Release

Production 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 Release

Troubleshooting

Common Issues

1. "Cannot find module 'expo-updates'"

Solution: Install expo-updates

npm install expo-updates

2. 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.jsonruntimeVersion
  • app.jsonios.runtimeVersion
  • app.jsonandroid.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.0

5. Download fails with fetch errors

Problem: expo-updates using wrong configuration Solution: Ensure:

  • updates.enabled: false in app.json
  • checkAutomatically: "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.