@deploy-your-app/cli
v0.6.2
Published
Ship over-the-air (OTA) live updates to Capacitor and Electron apps from the command line — bundle, sign, deploy, stage rollouts, and roll back, powered by DeployYourApp (https://deployyour.app).
Maintainers
Readme
@deploy-your-app/cli
dya — ship over-the-air (OTA) updates to Capacitor and Electron apps from
the command line. Bundle, sign, upload, roll out by percentage, and roll back
JavaScript/HTML/CSS releases without an app store review cycle.
🌐 deployyour.app
Website · Documentation · CLI reference · Pricing · Compare OTA platforms · Changelog · Dashboard
What this is
dya is the command-line tool for DeployYourApp, a
hosted OTA update, distribution, and diagnostics platform for Capacitor and
Electron apps. It sets a project up, generates the signing keys that stay on
your machine, and turns a built web directory into a signed bundle your
installed apps download and apply themselves.
Use it when you want to:
- Push a JavaScript/CSS fix to users the same day — "hot code push" / live update for Capacitor (iOS, Android) and Electron (Windows, macOS, Linux)
- Deploy from CI with an API key, on every merge
- Run
stagingandproductionchannels off a single native binary - Release to 10% of devices first, then roll back in one command
- Keep signing keys on your machine, never on a server
It needs a DeployYourApp account — every plan starts with a 30-day trial; see pricing.
The rest of the platform:
| Package | What it is |
|---------|-----------|
| @deploy-your-app/capacitor-update-manager | The Capacitor plugin that receives and applies these updates on iOS and Android |
| @deploy-your-app/electron-update-manager | The same live-update pipeline for Electron desktop apps |
Install
npm install -g @deploy-your-app/cli
dya --versionQuick start
The fastest path for a Capacitor app is the guided setup — it signs you in,
creates or links the app, generates signing/encryption keys, installs the
@deploy-your-app/capacitor-update-manager plugin, writes its config, and wires
notifyAppReady() into your app's entry point:
cd your-capacitor-app
dya setupIt never overwrites a source file it did not write. If src/dya.js (or
src/boot/dya.ts) is already yours, that file is left alone, the module lands
as dya-update, dya-ota, … and whichever name it takes is the one that gets
registered — and the one later runs keep using, rather than adding a second
module beside the first. Generated modules carry an @dya-generated marker
comment; delete that line to take ownership of the file.
Delete a generated module and leave its import './dya' behind, and the next
dya setup writes the file back — a registration with no module is a build that
fails on a dangling import, so it is not a state setup reports as "already
registered" and walks away from.
Then build your web assets and ship:
npm run build
dya deploy --channel productiondya init is an alias of dya setup — the same command under two names.
Both need credentials, a reachable server, and a terminal on stdin and
stdout — dya setup | tee setup.log is refused rather than started and then
silently answered.
Setup will not guess at your signing keys. If the project holds two signing
keypairs, or a dya-signing-public.pem that is not the public half of
dya-signing-private.pem, it stops with Setup stopped before writing
anything. — before the project config, the plugin install, or cap sync. The
publicKey it embeds is derived from the private key dya deploy signs with,
because that key is the only one your released apps will ever trust.
For the same reason it will not quietly replace a publicKey already embedded
in capacitor.config.json. That file is committed and .dya/* is not, so a
fresh clone of a shipped project has the public key and none of the private
half — and a keypair generated there is a different one, so every installed
app rejects everything signed with it, permanently. Replacing the embedded key
therefore takes typing replace the signing key; a bare Enter keeps the
embedded key and stops setup. Every refusal exits non-zero, so
dya setup && npx cap sync stops rather than syncing a half-configured project.
The same phrase now guards dya keys generate and the key-generation step of
setup. Minting a keypair while a different one is pinned is the same mistake
one step earlier, and the fresh-clone shape — pinned public key, no private half
anywhere — slips past every guard that only looks at keys on disk. Without a
terminal the command refuses and exits 1 rather than defaulting either way.
What it leaves behind
Everything the CLI owns lives in one folder at your project's web root:
.dya/
config.json # app ID, channel, assets path — commit this
dya-signing-private.pem # mode 0600 — never commit
dya-signing-public.pem
dya-encryption-public.pem
dya-encryption-private.pem # mode 0600 — never commitplus this block in your .gitignore:
# DeployYourApp — keys and local state. Never commit these.
.dya/*
!.dya/config.jsonCommit .dya/config.json and that block together. It has to be .dya/*
rather than .dya/: git cannot re-include a file whose parent directory is
excluded, so .dya/ would leave the config ignored — silently, since the file
still exists locally — and CI would check out a project with no app ID or
channel. You do not have to fix that by hand: a .dya/ line is detected,
replaced with .dya/*, and reported, leaving the rest of your .gitignore
untouched.
Nor do you have to check that it worked. After writing, the CLI asks real
git check-ignore whether .dya/config.json is still ignored, and reports the
file, line and pattern of anything that beat the block — a parent directory's
.gitignore, .git/info/exclude, your global excludes file — none of which it
will edit, because none of them are this project's file. With no git on
PATH, outside a repository, or when git does not answer in time, the lines are
still written but the success message carries the caveat — Written, but not
verified: git could not say whether .dya/config.json is still ignored. — and
prints the git check-ignore -v command to run yourself later. A check nobody
could perform is never reported as a check that passed.
setup and deploy write the block too, before moving a stray private key into
.dya/ — rules first, so an interrupted migration can only leave the key
ignored, never exposed. If the .gitignore cannot be written, nothing is moved.
Projects set up by an older CLI keep working untouched. A .deployyourapp file
at the project root and keys at the project root or in src-capacitor/ are still
read; they are just never written any more. setup, keys generate, and
deploy offer to move them into .dya/, and declining is fine.
Split project layouts
Quasar keeps the web app at the project root (package.json, src/,
quasar.config.ts) and the Capacitor project in src-capacitor/
(capacitor.config.json, android/, ios/, a second package.json). The CLI
resolves both and works from either directory — run it from inside
src-capacitor/ and it resolves up to the web root. In such a layout the plugin
is installed into both package.json files: the web root so the JS import
resolves, the Capacitor root so npx cap sync sees the native plugin.
Manual flow
dya login # opens the dashboard to authorize the CLI
dya keys generate # RSA-4096 signing + encryption keys into .dya/
dya deploy --channel production # bundle, sign, upload, deploydeploy needs appId and channel from .dya/config.json. Write it by hand if
you are skipping dya setup.
Signing in
dya login needs no configuration — it points at DeployYourApp out of the box.
ℹ Authenticate your account at:
https://app.deployyour.app/auth/cli-auth?session=cms9dczwb00006cu4ryck0hdc
Press ENTER to open in the browser...
- Waiting for you to authorize the CLI...
✓ Logged in as [email protected]The URL is printed first, so you can copy it into a different browser or another
machine instead of the default one. On a box with no desktop, dya login
--no-browser prints the URL and skips the launch.
Two-factor authentication is enforced by the browser flow when your account has
it enabled — the CLI never sees your password or authenticator secret, only the
30-day session token the browser hands back. It is stored with 0600
permissions in your OS config directory; dya logout clears it.
If your organization requires a 2FA code for uploads, dya deploy prompts for a
fresh authenticator code at upload time. That needs a terminal, so a pipeline
fails the upload with TWO_FACTOR_CODE_REQUIRED instead.
Commands
| Command | Description |
|---|---|
| dya login [--api-key <key>] [--no-browser] [--server <url>] | Authenticate (API key for CI; --no-browser for SSH/headless) |
| dya logout | Clear stored credentials |
| dya whoami | Show the signed-in account |
| dya setup [--app-id <id>] [--channel <name>] [--server <url>] | Guided setup for a Capacitor project (app, keys, plugin install + config, entry-point wiring) |
| dya init | Alias of dya setup |
| dya deploy [--channel] [--version] [--rollout] [--message] [--no-deploy] [--allow-older] [--allow-unsigned] [--path] [--target capacitor\|electron] | Bundle, sign, upload, and deploy (refuses unsigned bundles unless --allow-unsigned) |
| dya rollback [--channel] [--bundle <id>] | Roll a channel back to a previous bundle |
| dya apps list / dya apps create --name <n> --app-id <id> [--platform] | Manage apps |
| dya channels list / create --name <n> / delete --name <n> | Manage channels |
| dya keys generate [--app-id <id>] | Generate RSA-4096 signing + encryption key pairs |
| dya keys upload [--app-id <id>] | Upload existing public keys to the server |
| dya orgs list / dya orgs switch [--slug <slug>] | Manage organizations (single-org accounts are selected automatically) |
--no-deploy uploads the bundle without pointing any channel at it — release
it later from the dashboard. With no --version, the bundle version is your
package.json version plus a UTC build stamp, e.g. 1.4.0+20260804.143022 —
package.json must have a bare x.y.z version, or the deploy fails before
anything is built. --version itself also takes a bare x.y.z. --rollout
must be a whole number from 0 to 100.
deploy and rollback resolve --channel the same way — the flag, then
channel in .dya/config.json, then production — so a bare dya rollback
targets whatever a bare dya deploy shipped to. rollback prints the channel
before it does anything.
Code signing
dya keys generate writes two RSA-4096 key pairs into .dya/ (private keys
with 0600 permissions on macOS/Linux) and adds the .gitignore block for
you, creating the file if needed. Only the signing keypair is used by
dya deploy — bundles are never encrypted, and the encryption keypair it also
writes currently has no consumer; do not embed its private key in your app.
One signing keypair per project. Generation is refused when a signing key
already exists anywhere in the project, and if two distinct keypairs are found
the CLI prints each path and fingerprint, marks the one embedded in
capacitor.config.json, and exits 1 without writing anything — delete the
other and re-run. It never picks for you: installed apps
verify against the public key compiled into them, so signing a release with the
wrong key makes every installed app reject that update and every update after
it — with no server-side fix, only a new native release through the app stores.
Back the private keys up somewhere safe — a password manager or secrets
vault. Lose dya-signing-private.pem and you cannot ship another update your
installed apps will accept. Leak it and anyone can sign a bundle your users' apps
will install.
If you run dya deploy before any keys exist, it warns that an unsigned bundle
is refused by every installed client and offers to generate keys. In CI (no
TTY) it does not prompt — it declines, rather than minting a key that would
vanish with the build agent. If the Capacitor config already pins a
publicKey, the offer is withdrawn entirely: iOS, Android, and Electron all
require a valid signature before they will install anything, so a bundle
signed with a new, unpinned key is rejected by every installed app exactly as
permanently as an unsigned one — generating there would not be an
improvement. Either way, with still no signing key resolved, dya deploy
refuses to upload an unsigned bundle and exits 1 unless --allow-unsigned is
passed — and even then the result cannot install: the server rejects the
upload outright for an app that already has a signing key registered, and for
an app with none, no installed client will ever apply the result, because
signature verification is mandatory on every current platform.
dya deploy refuses to sign with a key your apps reject. Before signing,
it fingerprints the signing key against the publicKey pinned in the
Capacitor config and stops on a mismatch, exit 1, nothing uploaded — the
upload it prevents would have looked completely successful while every device in
the field rejected that bundle and every one after it. There is deliberately
no --force: an override on the publishing command gets pasted into a CI
script once and then suppresses the check forever. A real key rotation instead
goes through dya setup, which takes the typed consent and re-pins the config,
after which the fingerprints match honestly and the check passes on its own.
The pinned key is read from capacitor.config.json, .ts, or .js; a code
config is not executed, only scanned for a plain publicKey string literal.
When it cannot be read — code the literal is not in, invalid JSON, an unreadable
file — the CLI says the check could not be performed rather than reporting a
clean bill of health. Check plugins.DeployYourApp.publicKey yourself before
shipping a key change in such a project.
Bundle asset policy
There is no bundle encryption. dya deploy uploads a plain, signed ZIP — an
encrypted bundle is opaque to App Store review, so the pipeline instead makes
bundle contents transparent: anyone can unzip an uploaded bundle and see
exactly what ships. Before the ZIP is built, every file is checked against a
web-asset extension allowlist and, regardless of name, against a table of
native-executable magic bytes (Mach-O, ELF, Android DEX, PE/MZ, Java class);
any violation is reported and the deploy exits 1 before anything is zipped
or uploaded. The archive also carries a .dya-manifest.json with a per-file
SHA-256 hash of every entry, which every client re-derives and checks
bidirectionally during extraction.
Pointing at another environment
You almost certainly don't need this. The CLI targets
deployyour.app with no setup. --server exists for
working against a different DeployYourApp API — a local server during
development, say — and the choice is persisted:
dya login --server http://localhost:3000CI usage
Create an API key in the dashboard (Settings → API Keys), then:
dya login --api-key "$DEPLOY_YOUR_APP_API_KEY"
dya deploy --channel production --version "1.0.$GITHUB_RUN_NUMBER"Never run dya setup or dya init in a pipeline — they need a terminal on both
stdin and stdout, and exit 1 without one.
CI does not need them: .dya/config.json is committed, so the checkout is
already linked to the app.
Restore the signing key into .dya/ before deploying:
mkdir -p .dya
printf '%s' "$DYA_SIGNING_KEY_B64" | base64 -d > .dya/dya-signing-private.pem
chmod 600 .dya/dya-signing-private.pemOmit --version to auto-generate one. All commands exit non-zero on failure,
so a broken deploy fails the pipeline.
Links
| | | |---|---| | 🌐 Website | https://deployyour.app | | 📚 Documentation | https://deployyour.app/docs — quickstart, CLI commands, configuration, CI/CD | | 💰 Pricing | https://deployyour.app/pricing — 30-day trial on every plan | | 🔍 Compare | https://deployyour.app/compare | | 📊 Dashboard | https://app.deployyour.app | | 📝 Changelog | https://deployyour.app/changelog | | ✉️ Support | [email protected] |
Built and maintained by DeployYourApp. MIT licensed.
