Skip to main content

Commands

Every dash-ota command, its flags and their defaults. The same information is one command away: npx dash-ota <command> --help (or -h) prints a command's flags, which ones are required, and their defaults.

The examples assume @dash-ota/cli is installed in your app (npm i -D @dash-ota/cli) and are run from the app root. Outside such a project npx dash-ota works too: it downloads the dash-ota wrapper package, which runs the latest @dash-ota/cli.

Commands that talk to the backend take three shared flags, described in Overview:

FlagDefaultMeaning
--server <url>$OTA_SERVER, else http://localhost:4455backend base URL
--admin-token <token>$OTA_ADMIN_TOKENadmin credential; required, there is no default
--allow-insecureoffallow plain http:// to a host other than localhost

Input checks​

The CLI refuses input it can't use rather than guessing:

  • A flag a command doesn't accept is an error, with the closest match suggested:

    ✗ unknown flag for `rollout`:
    --rollout (did you mean --pct?)

    accepted: --bundle-id --pct --server --admin-token --allow-insecure --help
  • A flag that takes a value must get one: ✗ --pct needs a value (--pct <0-100>).

  • A switch takes no value: ✗ --mandatory is a switch and takes no value (got "yes").

  • Numbers must be whole numbers in range: ✗ --min must be a whole number from 0 to 2147483647 (got "2.5.0"). The ranges are 0–100 for percentages, 1–22 for --compression-level, and 1 or more for --bundle-version.

  • An unknown command prints ✗ unknown command "<name>" and exits 1.

Every error exits with status 1, so a CI step fails instead of carrying on.

keygen​

Generates an Ed25519 signing key pair and the channel content key.

npx dash-ota keygen --key-id key_prod_1
FlagDefaultMeaning
--out <dir>.keyswhere the key files are written
--key-id <id>key_dev_1names the files and the manifest keyId
--passphrase <passphrase>$OTA_KEY_PASSPHRASE, else a prompt in a terminalencrypts the private key
--no-encryptoffstore the private key unencrypted
--content-key-onlyoffonly add a missing <key-id>.content.key next to an existing signing key
--forceoffreplace an existing signing key (apps that embed the old public key then reject every later release)
--registeroffregister the new public key with the backend (needs --admin-token)
--interactiveoffask whether to register the key

It writes four files into --out: <key-id>.private.pem, <key-id>.public.pem, <key-id>.public.json and <key-id>.content.key, and prints publicKeyRawB64, the value your app embeds.

In a terminal it asks for a passphrase. An empty answer stores the key unencrypted and says so: ⚠ no passphrase entered: the signing key will be stored UNENCRYPTED. Without a terminal (CI) it never prompts: give --passphrase or OTA_KEY_PASSPHRASE, or --no-encrypt, otherwise it stops before writing anything:

✗ no passphrase for the signing key, and stdin is not a terminal to ask for one. Pass --passphrase <p> or set OTA_KEY_PASSPHRASE to encrypt it, or pass --no-encrypt to store it unencrypted.

Prefer OTA_KEY_PASSPHRASE over --passphrase: a command-line flag shows up in process listings and shell history. keygen refuses to overwrite an existing key unless you pass --force.

--server and --admin-token alone don't register anything; add --register. See Keys, custody & rotation.

register-key​

Tells the backend to trust a public key.

npx dash-ota register-key --key-id key_prod_1 --key-file .keys/key_prod_1.public.json
FlagDefaultMeaning
--key-id <id>key_dev_1the key id manifests carry
--key-file <path>—the <key-id>.public.json written by keygen
--pub <rawB64>—the public key as raw base64, instead of --key-file

One of --key-file or --pub is required.

fingerprint​

Prints the runtime version --runtime-version auto would use for this project.

npx dash-ota fingerprint --project .
FlagDefaultMeaning
--project <path>current directoryReact Native app root

The value is a hash of every dependency in package.json (name and version range as written), the React Native and Hermes versions, and the files under android/ and ios/. Any dependency change moves it, including a JavaScript-only one. Inside a git work tree only files git tracks under android/ and ios/ count, so machine-local files such as local.properties or .xcode.env.local don't make your laptop disagree with CI.

The app has to embed the same value, which is why the quickstart uses a fixed string such as rt1 instead. See Versioning & targeting.

bundle​

Runs react-native bundle into a payload directory, optionally compiled to Hermes bytecode.

npx dash-ota bundle --project . --platform android --out ./ota-out/android --hermes
FlagDefaultMeaning
--project <path>current directoryReact Native app root
--platform ios|androidandroidtarget platform
--out <dir><project>/.dash-ota-bundle/<platform>payload directory for publish
--entry <file>index.jsJavaScript entry file, relative to --project
--devoffbuild a development bundle
--hermesoffcompile to Hermes bytecode with the app's own hermesc; fails if there is none

Keep --out the same for every release. The server stores each distinct file once, so a stable path lets unchanged files be skipped. hermesc is found in react-native/sdks/hermesc (React Native 0.82 and older) or the hermes-compiler package (0.83 and later). See Hermes & HBC.

publish​

Compresses, encrypts and signs a payload directory, then uploads only the files the server doesn't already have.

npx dash-ota publish --bundle-dir ./ota-out/android --app-id com.example.app \
--platform android --channel prod --key-id key_prod_1 \
--runtime-version rt1 --bundle-version 7 --rollout 10 \
--release-note "Fix order confirmation crash"
FlagDefaultMeaning
--bundle-dir <dir>— (required)payload directory written by bundle
--app-id <id>— (required)Android applicationId / iOS bundle id; devices refuse a release for another app
--platform ios|androidandroidtarget platform
--channel dev|uat|proddevrelease channel
--runtime-version auto|<value>automust equal the app's embedded runtime version; auto fingerprints --project
--bundle-version <n>1must be higher than the version the device runs
--min-native-build <n>—devices with a lower native build number skip this release
--mandatoryoffdevices download and apply it without asking
--target-app-versions <range>—semver range over the app version, e.g. ">=1.2.0 <1.3.0"
--rollout <0-100>100percentage of devices offered the release
--release-note <text>—"What's new" text
--bundle-id <id>bnd_<runtime>_<version>_<time>release id; a new one on every run
--key <path>.keys/<key-id>.private.pemsigning key
--key-id <id>key_dev_1signing key id, registered with the backend
--passphrase <passphrase>$OTA_KEY_PASSPHRASEdecrypts an encrypted signing key
--verify-pub <rawB64><key-id>.public.json next to --keypublic key the signature is checked against before upload
--no-encryptoffstore compressed plaintext; files are still hash-checked
--content-key <base64>$OTA_CONTENT_KEY, else <key dir>/<key-id>.content.keychannel content key
--compression-level <1-22>19zstd level for the JS bundle
--no-uploadoffwrite the signed release next to --bundle-dir instead of uploading it
--project <path>current directoryapp root that --runtime-version auto fingerprints
--interactiveoffprompt for anything not given

Output from a real run:

✓ self-verified signature (.keys/key_dev_1.public.json)

bundleId: bnd_rt1_2_mumketyh
runtimeVersion: rt1 bundleVersion: 2
files: 1 (1 distinct blobs) 1.25 MB → 481.4 KB
encryption: aes-256-gcm
uploading: 1 of 1 blobs (0 already present) rollout: 100%
uploaded 1/1
✓ published to http://localhost:4455: {"ok":true,"bundleId":"bnd_rt1_2_mumketyh","rolloutPercentage":100,"already":false}

The self-verify step checks the signature against the public key next to your private key (or --verify-pub). It catches a damaged or mismatched key pair. It can't know which key your app embeds: if that differs, every device rejects the release with manifest signature did not verify.

A publish runs in three steps: declare the release, upload the missing files, finalize. Devices are never offered a release until it is finalized. Re-running an interrupted publish creates a new release with a new bundleId; the unfinished one shows as INCOMPLETE in list and is never offered. A finalized release can't be changed: publishing the same bundleId again returns 409.

Every release on a channel must use the same content key. keygen writes it next to the signing key, and publish stops if it can't find it rather than making one up, because a new key per release would re-upload every file every time.

--min-native-build stops a release reaching devices on an older store build. It sets a minimum only: it doesn't stop an older release from installing on a newer build. The way to separate native builds completely is a new --runtime-version for each store build that changes native code.

list​

npx dash-ota list
bnd_rt1_2_mumkmbz7 [android/dev] rt=rt1 v2 100% adoption={"applied":1,"healthy":1,"failed":0,"rolled_back":0}

Each line shows the release, its platform and channel, runtime version, bundle version, rollout state and what devices have reported. The rollout column reads PAUSED, ROLLED_BACK or INCOMPLETE instead of a percentage when that applies.

rollout, pause, rollback​

npx dash-ota rollout --bundle-id <id> --pct 50
npx dash-ota pause --bundle-id <id> # add --resume to offer it again
npx dash-ota rollback --bundle-id <id>
CommandFlags
rollout--bundle-id (required), --pct <0-100> (default 100)
pause--bundle-id (required), --resume
rollback--bundle-id (required)

publish sets the first percentage with --rollout; the rollout command changes it with --pct. The flag guard catches the mix-up.

A device is in a release's rollout when its bucket (the first 32 bits of sha256(installId:bundleId), mod 100) is below the percentage, so each release is offered to a different set of devices, and raising the percentage only ever adds devices.

rollback pauses the release and flags it rolled back: ✓ release rolled back (paused + flagged). pause --resume lifts the pause but not the flag, nothing clears the flag, and there is no delete. A download already in progress gets 410 on its remaining files. Devices that already installed the release keep it; to replace it, publish a fixed release with a higher --bundle-version. See Rollback.

native-policy​

Sets a channel's force-update gate: the lowest native build number the channel still supports.

npx dash-ota native-policy --channel prod --min 42 --severity soft
FlagDefaultMeaning
--channel dev|uat|proddevchannel the policy applies to
--min <build>0lowest native build number still supported
--severity soft|hardhardwhat devices below --min are told; your app decides what each looks like
--store-url <url>—store link sent with the policy

Mind the defaults: native-policy --min 5 alone sets a hard gate on the dev channel.

The backend accepts only https://, market:// and itms-apps:// store links. Clients from 0.5.0 ignore the server's link and use config.storeUrl from the app, so --store-url only matters for older clients.

dashboard​

Serves a local web page for the commands above, on 127.0.0.1 only.

npx dash-ota dashboard --config dash-ota.config.mjs
FlagDefaultMeaning
--config <path>dash-ota.config.mjsenvironments file
--port <n>4460port on 127.0.0.1; 0 picks a free one
--no-openoffprint the link without opening a browser

See Dashboard.