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-ota

v0.3.0

Published

Publish Expo updates to a self-hosted Open OTA server

Downloads

908

Readme

open-ota

Publishes an Expo app's JS bundle to a self-hosted Open OTA server. Follow the server setup guide first, then install the CLI in your app so local runs and CI use the same version:

npm install --save-dev --save-exact open-ota

Run commands with npx open-ota from the app directory. The examples below use open-ota as shorthand.

npx open-ota publish --branch staging --message "Fix payment sheet"

It checks configuration, credentials, signing, and active rollouts before exporting both platforms. It resolves each platform's runtime version, uploads missing assets, computes bsdiff patches against the bundles the server ranks as worth it (what devices run, registered builds, recent publishes), verifies each one, and then publishes the update group. Patches go first so no device can fetch the new bundle before its patch exists. The engine ships with the CLI as WebAssembly; nothing needs to be installed.

Credentials

| Setting | Env var | Flag | Value | | --- | --- | --- | --- | | Server origin | OTA_URL | --server | The updatesUrl printed by the server deploy, for example https://u.example.com | | Publish token | OTA_PUBLISH_TOKEN | --token | The OTA_PUBLISH_TOKEN the server was deployed with | | Actor | OTA_ACTOR | | Optional. Who the publish is credited to; defaults to the commit author |

Flags win over environment variables. In CI, store the token as a secret and the origin as a variable. Locally, export them in your shell or put them in a .env your shell loads; the CLI does not read .env files itself.

Commands

open-ota init --channel <name> [--certificate ./certs/certificate.pem]
open-ota doctor [--channel <name>] [--platform ios|android]
open-ota publish --branch <name> [--message <text>] [--rollout <0-100>]
                 [--platform ios] [--platform android] [--skip-export] [--dist <dir>]
                 [--no-patches] [--project <dir>] [--server <url>] [--token <token>]
open-ota rollback-to-embedded --branch <name> [--platform ios|android] [--message <text>]
open-ota patches --branch <name> [--platform ios|android] [--project <dir>]
open-ota build register --platform ios|android --profile <name> [--distribution store|internal|simulator]
                        [--channel <name>] --manifest <app.manifest> --bundle <bundle>
                        [--runtime <version>] [--project <dir>]
open-ota build get --platform ios|android --profile <name> [--distribution store|internal|simulator]
                   [--channel <name>] [--runtime <version>] [--include-inactive] [--project <dir>]
open-ota build deactivate --id <build-id>
open-ota build activate --id <build-id>
open-ota --help

--rollout starts the group as a canary; the remaining devices keep the previous update until the rollout is widened in the dashboard. Only one active rollout is allowed per branch, platform, and runtime. Complete it at 100% or use Roll back before publishing or promoting another update for that build. The server enforces this atomically, including concurrent publishers. Rollout percentages cannot decrease.

rollback-to-embedded publishes a group that sends devices matching the selected platforms and the runtime versions resolved from this project back to the JS baked into their builds. Other runtime versions are unaffected. Publish-only flags such as --rollout are rejected for rollback.

Requires Node 20 or newer and the Expo CLI of the app it runs in.

Delta patches

publish asks the server for the bundles worth diffing the new one against, diffs each, checks that the patch rebuilds the new bundle, and uploads it. A patch is only uploaded when it is at most the server's configured share of the compressed bundle (30% by default); the server checks the same rule and rebuilds the bundle from the patch before storing it. Skipped and declined patches are reported with --verbose; a failure is a warning and the update still publishes.

open-ota patches --branch production computes the patches the newest bundle on the branch is missing, from every base the server reports, skipping bases already covered. Run it after the fleet moved on or after a publish with --no-patches.

Build registry

build get answers whether a compatible native build was registered for a platform, runtime, profile, distribution and optional channel. When --runtime is omitted, the CLI resolves the current project's runtime version. The command returns successfully when no build matches; its JSON result contains "build": null, which makes it suitable for deciding between an OTA and a native build in CI.

The channel is a compatibility constraint, not a label: a build only receives updates on the channel baked into it, so CI should always pass --channel when deciding whether an OTA update is enough. Omitting it matches builds on any channel, like the EAS build list does.

Register builds only after the delivery step your workflow considers successful. For production, that normally means after the store submission succeeds. Registering the same build again, for example after a resubmission, updates its record in place and does not upload the bundle twice.

A registered build proves a compatible artifact was delivered, not that it is still available. When a submission is rejected or a build is pulled, build deactivate --id <build-id> keeps the record and its embedded bundle, so existing installs still get delta patches and retention still protects the bundle, but build get stops returning it. build activate reverses that, and so does registering the build again. Both are safe to repeat. Pass --include-inactive to build get to see the newest matching build regardless of state; every result carries an active field.

Patching fresh installs

A fresh install runs the JavaScript embedded in its build. The expo-updates client offers a patch against that bundle like any other, but the server can only answer if it holds the bundle's bytes, and nothing uploads them on its own. build register stores the build and uploads its embedded bundle under the update id in its manifest. This patch path is implemented and tested against the server, but it has not been verified on a device yet, so watch a fresh install's first update in the dashboard's patches panel before relying on it.

The files come from the built artifact, not from expo export:

# iOS: the .app produced by the archive
open-ota build register --platform ios --profile production --channel production \
  --manifest build/YourApp.app/app.manifest --bundle build/YourApp.app/main.jsbundle
# Android: unzip the APK or AAB first
open-ota build register --platform android --profile production --channel production \
  --manifest apk/assets/app.manifest --bundle apk/assets/index.android.bundle
open-ota patches --branch production

The update id comes from the manifest, the runtime from --runtime or from the project.

Set up and check an app

Install expo-updates in the app first with npx expo install expo-updates. Copy the server's public certs/certificate.pem into the app, configure credentials, and choose an existing channel. New deployments include staging and production; create any additional channels in the dashboard.

open-ota init --channel staging
open-ota doctor

init verifies credentials and the server's signature before changing files. For static app.json, it sets fingerprint runtimes, the update URL, channel, and code signing. Existing unrelated fields are preserved and the original is saved as app.json.open-ota.bak; an existing backup is never replaced. It runs doctor after writing. Review the diff and ship a new native build.

For app.config.ts or JavaScript config, init prints a JSON snippet without editing the file. Merge it into the returned Expo config, keeping environment-specific channel selection, then run doctor with the same environment as the native build. URL, channel, and certificate changes need a new native build; they cannot reach installed apps through an OTA update.

doctor checks resolved Expo config, certificate validity, server authentication, channel mapping, per-platform runtime versions, fleet observations, and a signed manifest or directive. --channel asserts the configured channel; it does not override the app. No observed devices is a warning for a fresh app, and a signed no-update response proves connectivity and signing, not device delivery. Doctor probes do not register fake devices. Local Expo commands and server checks have a shared 60-second deadline. Publish runs these checks before exporting, including with --skip-export.

Both commands accept --project, --server, --token, --platform, --verbose, and --json. Doctor JSON contains channel, branch, and per-platform runtimes. Init JSON has a mode of configured (with check results) or snippet (with fields to merge).

Terminal output

Interactive terminals show a spinner for each stage, elapsed times, and completed asset counts. CI, redirected output, TERM=dumb, and --verbose use plain progress lines. Set NO_COLOR to disable colors. Progress, warnings, and the human-readable summary go to stderr.

Use --verbose to stream subprocess output and show individual patch diagnostics. Patch failures are warnings: the update still publishes and devices can download full bundles. Pass --no-patches to skip them and open-ota patches to add them later.

Use --json to write one result object to stdout for scripts:

npx open-ota publish --branch staging --json > published.json
npx open-ota build get --platform ios --profile production --channel production --json > build.json

The publish object contains command, server, branch, message, groupId, and updates (each with id, platform, and runtimeVersion). Publish results also include rolloutPercent. Build commands return command, server, and build; a lookup uses null when no build matches, and every build carries active. Progress remains on stderr. Fatal errors exit with status 1 and do not write a result object; optional patch failures still return a successful result.

open-ota --version prints the installed version. Run open-ota publish --help or open-ota rollback-to-embedded --help or open-ota build --help for command-specific options and examples.

Command parsing, validation, help, and version output use Effect 4's effect/unstable/cli. Generate shell completions with open-ota --completions bash (also supports zsh, fish, and sh).

Publishing runs inside the command's Effect scope. File access, HTTP requests, subprocesses, and progress use Effect services; interrupting the command cancels in-flight work and releases temporary patch directories. Tokens remain redacted until the HTTP or terminal boundary.