Ship JavaScript updates from infrastructure you own.
The backend runs on a server you already pay for, and no vendor service sits between your CI and your users’ devices.
Signing can’t be switched off: every release is signed in your CI and checked by native code in the app. The update server never holds the signing key, so an attacker with root on it cannot ship their own code to your users.
New to OTA updates? Start with what an OTA update is, then check the prerequisites.
MIT · Android + iOS · React Native 0.79+ (New Architecture)
# in your app, with a dash-ota backend on localhost:4455 and# OTA_ADMIN_TOKEN and OTA_KEY_PASSPHRASE set in your shell$ npm i -D @dash-ota/cli$ npx dash-ota keygen --register✓ wrote keypair to .keys/key_dev_1.*✓ registered key_dev_1 with http://localhost:4455$ npx dash-ota bundle --platform android --out ./out --hermes$ npx dash-ota publish --bundle-dir ./out --app-id com.example.app --runtime-version rt1 --bundle-version 2✓ self-verified signature (.keys/key_dev_1.public.json)bundleId: bnd_rt1_2_mumketyhuploading: 1 of 1 blobs (0 already present) rollout: 100%✓ published to http://localhost:4455: {"ok":true,"bundleId":"bnd_rt1_2_mumketyh","rolloutPercentage":100,"already":false}
Every release is on trial until it proves itself.
An update applies on the next cold start, or when your app asks for a restart. It stays on trial until your app calls markHealthy(). A launch that crashes counts against it; a session that reached JavaScript and went to the background doesn’t. If it is still on trial at its third launch, the device disables it and goes back to the last working bundle, without a deploy.
A fresh install runs the bundle compiled into the app.
launch: no stored bundle — using the embedded one
Someone has root on your update server.
What root on your server does and doesn’t allow:
They cannot
Forge a release
The server has no signing key. A manifest it edits fails Ed25519 verification in native code, before anything is written.
Swap a file inside one
Every file’s SHA-256 is in the signed manifest, and native code re-hashes each file after decrypting it.
Move a build to another app or channel
appIdandchannelare signed. The device checksappIdagainst its own package or bundle id and, from 0.5.1,channelagainst the value compiled into the binary.Install an older bundle over a newer one
Native code refuses a
bundleVersionthat isn’t higher than the bundle running now.
They still can
Stop serving updates
A device can’t tell silence from “up to date”. Alert when your release pipeline goes quiet.
Re-serve an older or withdrawn release
Anything you signed stays valid, and pause and rollback are server state. The downgrade check compares only with what runs now, so after a store update, a
rollback()or a crash-loop revert, an older signed release can install again.Force a hard update prompt
nativePolicyisn’t signed. A breached server can send severityhardto every install, and if your app shows that as a blocking screen, users are locked out while it lasts. From 0.5.0 the store link comes from your app’s config, not the server.Read your bundles
The content key is in the manifest the server stores and returns to enrolled devices, so encryption doesn’t hide bundles from whoever runs the server.
A console that runs on your laptop.
Publish, ramp a rollout, pause, roll back and set the force-update policy from a web UI bound to 127.0.0.1 and gated by a token minted per launch. It talks only to the backends in its config file, and that file holds your key paths and admin tokens.
- Release table with adoption and rollout state
- Live publish log while it builds and uploads
- Typed confirmation on protected environments
- Read-only for any environment with no admin token
# with dash-ota.config.mjs in the project and OTA_ADMIN_TOKEN set$ npx dash-ota dashboarddash-ota dashboard → http://127.0.0.1:4460/#t=…local only (127.0.0.1) · the link carries this session's token · Ctrl+C to stop$ npx dash-ota listbnd_rt1_2_mumketyh [android/dev] rt=rt1 v2 100% adoption={"applied":1,"healthy":1,"failed":0,"rolled_back":0}$ npx dash-ota rollback --bundle-id bnd_rt1_2_mumketyh✓ release rolled back (paused + flagged)
Drops into what you already have.
Autolinked on both platforms. One edit each in MainApplication.kt and AppDelegate.swift points React Native at the OTA bundle, and the channel, server URL, public key and runtime version go in string resources on Android and Info.plist on iOS. The library ships its own ProGuard rules and needs no Podfile entry.
import AsyncStorage from '@react-native-async-storage/async-storage';import { DashOtaProvider } from 'react-native-dash-ota';export default function Root() {return (<DashOtaProviderconfig={{appVersion: '1.4.0',storage: AsyncStorage,// your own API: a session token the backend checks in verifyEnrollTokengetEnrollToken: () => api.otaEnrollToken(),}}><App /></DashOtaProvider>);}
Your first signed update, end to end.
The quickstart runs a backend, makes a key, wires the app, publishes and rolls back, and shows the output to expect at each step. New to OTA updates? Read what an OTA update is and the prerequisites first.