Complete reference for the dya binary. Commands are grouped by area: auth, project, deploy, apps, channels, keys, and organizations.
Global options
| Option | Type | Default | Description |
|---|---|---|---|
-h, --help |
flag | — | Show help for the program or a command. Exits 0. |
-V, --version |
flag | — | Print the CLI version, read from the installed package's package.json. Exits 0. |
Most commands operate on two pieces of context:
- The active organization, stored in the global config. If your account belongs to exactly one organization, it is resolved and cached automatically the first time a command needs it —
deploy,rollback,apps,channels, andkeysall work without ever runningdya orgs switch. With more than one organization and none selected, those commands fail and tell you to rundya orgs switch. - The project config,
.dya/config.jsonat your project's web root, written bydya setup. The legacy.deployyourappfile at the project root is still read when there is no.dya/config.json. Commands resolve the project by walking up from the directory you run them in, so they work from anywhere inside it. See Configuration.
Every command exits non-zero on failure. Usage errors (an unknown command, a missing required flag) exit 1 via the argument parser; runtime failures exit 1 after printing the server's error message.
Auth commands
dya login
Authenticates the CLI. With no flags it runs the browser flow; with --api-key it stores a key for non-interactive use. Either way the credential is verified against the server before login is reported as successful, and a rejected credential is deleted rather than left on disk.
| Option | Type | Default | Description |
|---|---|---|---|
--api-key <key> |
string | — | Use an API key instead of browser auth. Validated with a request to /api/orgs. |
--no-browser |
flag | — | Print the authorization URL instead of opening a browser. For SSH sessions, containers, and machines with no desktop. |
--server <url> |
string | — | API server base URL, for a non-default environment such as a local development server. Trailing slashes are stripped and the value is persisted in the global config. Leave it unset to use DeployYourApp. |
Storing one credential clears the other: an API key login deletes any stored session token, and a browser login deletes any stored API key.
dya login
ℹ 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 ada@example.com
The URL is printed before anything opens, so you can copy it to another machine
or browser profile rather than using the default one. --no-browser skips the
ENTER prompt and the launch entirely.
If your account has two-factor authentication enabled, the browser flow enforces it — the CLI never handles your password or authenticator secret, only the session token the browser hands back.
# Non-interactive, for CI
dya login --api-key "$DEPLOY_YOUR_APP_API_KEY"
How the browser flow works
The CLI opens a pending login on the server, shows you where to approve it, and polls until you do. Nothing listens on your machine, so this works over SSH, inside containers, and when your browser runs on a different host entirely.
POST /api/auth/cli-auth/sessioncreates a pending request and returns a session id, a secret poll token, and the login URL.- You approve in the browser. The page shows which client is asking, the IP it came from, and when — so a request you did not start is recognisable.
- The CLI polls
GET /api/auth/cli-auth/session/:id, which answers202while pending and200with the token once approved.
The session id appears in the login URL; the poll token never does, and travels
only in the X-Poll-Token header. Knowing a URL you pasted into chat is
therefore not enough to steal the credential. Approval is single-use — a second
claim gets 410, so a replayed poll cannot mint a second token.
A pending request expires after 10 minutes. Denying in the browser fails the waiting terminal immediately rather than leaving it to time out. A dropped network connection does not end the flow; the CLI keeps polling.
The returned token is a 30-day session, written to the global config with 0600
permissions.
CLI 0.1.x used a localhost callback server on ports 9876–9895. The server
still accepts that flow so already-installed copies keep working.
dya logout
Deletes the stored session token and API key from the global config. Takes no options and makes no network request.
dya logout
✓ Logged out
dya whoami
Shows the current credential. With a session token it prints your name and email from /api/auth/get-session. With an API key it prints the key prefix plus the organizations the key's owner belongs to, because API keys have no session to report.
dya whoami
Logged in as:
Name: Ada Lovelace
Email: ada@example.com
Org: org_2f8c1d94ab
With an API key:
ℹ Authenticated with API key (dya_1a2b...)
Orgs: acme
Active org: org_2f8c1d94ab
Exits 1 when nothing is stored, when the session has expired, or when the key is no longer valid.
Project commands
dya setup
Interactive wizard for a Capacitor project: authenticate, pick or create the app, write the project config, generate keys, install the plugin, write the plugin configuration, and wire an init module into your app's entry point. Requires a terminal on both stdin and stdout — anywhere else, including behind a pipe, it prints an error and exits 1.
dya init is an alias of this command. The two names run the same code.
| Option | Type | Default | Description |
|---|---|---|---|
--app-id <appId> |
string | — | App ID, for example com.example.app. Skips both the existing-app picker and the app-ID prompt. Must start with a letter and contain only letters, numbers, ., _, or -; an invalid value exits 1 before anything else runs. |
--channel <channel> |
string | — | Default channel written to .dya/config.json. Skips the channel prompt. Omit it and you are prompted, with production offered as the default answer. |
--server <url> |
string | — | API server base URL, for a non-default environment such as a local development server. Persisted in the global config. |
cd your-capacitor-app
dya setup
The steps, in order:
- Persists
--serverif given. - Resolves the project layout — the web root, the Capacitor root, and the framework — from wherever you ran the command. Offers to move any keys or a legacy
.deployyourappconfig into.dya/. - Checks that the project's signing keys are unambiguous, and stops before writing anything if they are not — see Setup refuses ambiguous signing keys below.
- Runs the browser login when neither a token nor an API key is stored. Declining exits
1. - Resolves the organization, auto-selecting when there is exactly one, otherwise showing a numbered picker. An account with no organization exits
1. - Detects
package.jsonand@capacitor/coreand asks for confirmation before continuing if either is missing. - Offers the organization's existing apps plus a "Create a new app" entry. New apps are created with platform
bothand app IDs are re-prompted until they validate.--app-idskips the prompt. - Prompts for a default channel and writes
.dya/config.json.--channelskips the prompt. - Offers
dya keys generatewhen the project has no signing key. When the Capacitor config already pins apublicKeythat nothing on this machine can sign for, the y/N offer is replaced by a typed phrase — see Setup will not mint a keypair over a pinned one below. - Offers to install
@deploy-your-app/capacitor-update-managerwith pnpm, yarn, or npm, chosen from the lockfile present. In a split layout it is installed into bothpackage.jsonfiles — the web root so the JS import resolves, the Capacitor root sonpx cap syncsees the native plugin. - Writes the init module and registers it in your app's entry point — see Generated init module below.
- Merges
appId,channel, and the signingpublicKeyintoplugins.DeployYourAppincapacitor.config.json. ThepublicKeyis derived from the private keydya deploywould sign with, not read on trust from whichever.pemhappens to be nearby, and a key already embedded incapacitor.config.jsonis never replaced without typed consent — see Replacing an embedded signing key needs a typed phrase below. For.tsand.jsconfigs it prints a snippet instead of rewriting code.updateUrlandstatsUrlare added only when the CLI is pointed at a non-default server. - Offers to run
npx cap sync, in the Capacitor root rather than the web root.
Nothing is written invisibly: the init module and the entry-point edit are echoed as + Created / ~ Modified with their paths, and the other writes announce themselves individually (✓ Wrote .dya/config.json, ✓ Updated capacitor.config.json with the DeployYourApp plugin config, ✓ Added to .gitignore: …, ✓ Moved <from> → <to>).
Setup requires a terminal on stdin and stdout
Every question in the wizard — including the stray-key migration, which is asked before anything else — falls back to a default answer when it cannot be asked. Rather than run half a wizard whose questions answer themselves, setup checks both streams up front and refuses:
✗ `dya setup` (and `dya init`, which is now the same command) is interactive, and needs a
terminal on both stdin and stdout. In CI, script the individual commands instead
(login, keys, deploy).
So dya setup | tee setup.log is rejected too, not only a pipeline with no TTY at all.
Setup refuses ambiguous signing keys
Setup is the command that bakes a publicKey into your app, and that key is the only one an installed release will ever verify a bundle against — no server-side change can revise it later. So before the project config is written, before the plugin is installed and before npx cap sync is offered, setup checks two things and stops on either. Nothing is written when it stops.
Two signing keypairs. The same check dya keys generate applies, run on every setup regardless of whether keys are being generated — a project that already has two keypairs never reaches the generator's guard. Each path and fingerprint is listed, the one matching the embedded publicKey is marked, and setup ends with:
✗ Setup stopped before writing anything. Re-run `dya setup` once one keypair is left.
If a second keypair somehow appears mid-run, the key-generation step catches it and stops with Setup stopped: no keys were generated, the Capacitor plugin was not configured / and `npx cap sync` was not run. Re-run `dya setup` once one keypair is left.
A public key that is not the private key's public half. dya-signing-public.pem is never taken on trust: setup fingerprints it against the public half derived from the dya-signing-private.pem that dya deploy would sign with, and a disagreement is fatal:
✗ The signing public key in this project is NOT the public half of the signing private key.
.dya/dya-signing-private.pem e2bbb0fc4a91d3e7 ← what `dya deploy` signs with
.dya/dya-signing-public.pem 59c5420cb8d7f102 ← what would have been embedded
⚠ Nothing was written to the Capacitor config. Embedding that public key would make
⚠ every installed app verify updates against a key nothing here can sign with, so
⚠ every update would be rejected — and no server-side change could fix it.
Delete the stray public key — one can always be re-derived from its private half — or restore the private key it belongs to, then run dya setup again.
When the keys do agree, the key that will be embedded is announced with its fingerprint and where it came from:
ℹ Signing public key to embed: e2bbb0fc4a91d3e7 (derived from .dya/dya-signing-private.pem)
With a public .pem on disk that matches, the file's own bytes are used and the source reads (.dya/dya-signing-public.pem, verified against .dya/dya-signing-private.pem) instead.
Two cases warn rather than stop, because there is a config worth writing without a publicKey in it — setup continues and simply embeds none:
- The private key cannot be read.
Could not read <path> as a private key: <reason>, followed by a warning that an app trusting a key you cannot sign with rejects every update. - A public key with no private half.
<path> exists but its private half does not.Nothing in the project could sign a bundle that key verifies, so restoring the private key or runningdya keys generatecomes first.
Setup will not mint a keypair over a pinned one
The offer to generate keys only comes up when the project has no signing private key at all, and it is the neutral Generate RSA signing + encryption keys now? only when nothing is at stake. If the Capacitor config already pins a publicKey in that state, the pinned key is one nothing here can sign for, and generating a keypair breaks every app that shipped with it — so the y/N offer is withdrawn and the same typed phrase used for the embed step is demanded instead:
✗ This project already ships a signing public key that nothing here can sign for.
src-capacitor/capacitor.config.json e2bbb0fc4a91d3e7 ← what installed apps verify against
a new keypair, with a fingerprint of its own ← what would be generated here
⚠ Every app already installed — from a store build or an earlier OTA update — checks
⚠ updates against e2bbb0fc4a91d3e7. Changing the key here does not reach those apps.
⚠ They go on trusting e2bbb0fc4a91d3e7, every bundle you sign from now on is signed
⚠ with the new key, and every one of those updates is rejected. Permanently: no
⚠ server-side change, no re-deploy and no later `dya setup` can undo it — only a new
⚠ store release built with the new key can.
⚠ The Capacitor config is committed and `.dya/*` is not, so a fresh clone of a project
⚠ that has already shipped lands here. If that is what happened, get the private key for
⚠ e2bbb0fc4a91d3e7 from whoever made the first release — do not replace it.
⚠ Go ahead only if no build carrying e2bbb0fc4a91d3e7 was ever installed anywhere.
? Type "replace the signing key" to generate a DIFFERENT keypair anyway, or press Enter to skip:
Anything but that sentence — a bare Enter included — leaves the pinned key alone and names the file the missing private key has to land in:
⚠ Skipping keys — the app stays pinned to e2bbb0fc4a91d3e7, which nothing
⚠ here can sign for. Put that private key in .dya/dya-signing-private.pem
⚠ and run `dya setup` again before deploying.
With no key pinned, declining is the ordinary ⚠ Skipping keys — bundles will be unsigned until you run `dya keys generate`. and setup carries on. That is not a soft warning: every current iOS, Android, and Electron client refuses any bundle it cannot verify, so a bundle deployed before a signing key exists is refused by every installed app, not merely unverified. Nothing updates until a signing key exists and dya deploy signs with it.
Replacing an embedded signing key needs a typed phrase
Everything above only proves that the key setup is about to embed pairs with the key dya deploy signs with. It says nothing about the key already in capacitor.config.json — the one installed apps actually verify against — and step 13 merges the new plugin config over the old one. So before that merge, setup compares the two fingerprints, and a difference stops it.
This is the normal shape of a fresh clone, not an exotic case. capacitor.config.json is committed and .dya/* is gitignored, so a second developer has the shipped public key and none of the private half; if they let setup generate keys, what it generates is internally consistent — it passes every pairing check above — and simply is not the key every installed app trusts.
✗ This project already ships a DIFFERENT signing public key.
src-capacitor/capacitor.config.json e2bbb0fc4a91d3e7 ← what installed apps verify against
.dya/dya-signing-private.pem 59c5420cb8d7f102 ← what would be written, and what `dya deploy` signs with
⚠ Every app already installed — from a store build or an earlier OTA update — checks
⚠ updates against e2bbb0fc4a91d3e7. Changing the key here does not reach those apps.
⚠ They go on trusting e2bbb0fc4a91d3e7, every bundle you sign from now on is signed
⚠ with 59c5420cb8d7f102, and every one of those updates is rejected. Permanently: no
⚠ server-side change, no re-deploy and no later `dya setup` can undo it — only a new
⚠ store release built with the new key can.
⚠ The Capacitor config is committed and `.dya/*` is not, so a fresh clone of a project
⚠ that has already shipped lands here. If that is what happened, get the private key for
⚠ e2bbb0fc4a91d3e7 from whoever made the first release — do not replace it.
⚠ Go ahead only if no build carrying e2bbb0fc4a91d3e7 was ever installed anywhere.
? Type "replace the signing key" to replace it, or press Enter to keep it:
Deliberately not a y/N question: y is the answer people give without reading, and this one cannot be taken back. The answer is trimmed and compared case-insensitively against that one sentence; anything else — a bare Enter included — keeps the pinned key and ends the run without writing:
ℹ Kept e2bbb0fc4a91d3e7. Nothing was written to src-capacitor/capacitor.config.json.
✗ Setup stopped: the plugin was not configured and `npx cap sync` was not run.
Typing the phrase writes the new key, and says what that costs:
⚠ Replacing e2bbb0fc4a91d3e7 with 59c5420cb8d7f102 on your say-so.
⚠ Every app already carrying the old key is now unreachable by OTA update: ship a new
⚠ store build signed with the new key before you deploy.
Two quieter outcomes, so a re-run is never ambiguous about what happened to the embedded key:
- The key setup would write is the embedded one:
ℹ Already embedded in <config> — unchanged. - Setup has no key of its own to write and one is pinned:
ℹ Leaving the embedded signing key <fingerprint> in <config> untouched.The merge preserves it.
Every setup refusal exits 1
Setup stopped … is not just a message. Each of the refusals above — two keypairs, a mismatched public key, a declined key replacement — sets a non-zero exit code before returning, so dya setup && npx cap sync stops there instead of syncing a project the wizard deliberately left unconfigured. The process still unwinds normally, so the prompt is closed cleanly rather than killed mid-question.
Reading the pinned key out of a .ts or .js config
The key already pinned in the Capacitor config is read from capacitor.config.json, capacitor.config.ts, or capacitor.config.js — whichever Capacitor itself would resolve first. A .ts or .js config is code, and the CLI does not execute your config to read it. It looks for a plain publicKey string literal and accepts it only if it parses as a public key. That covers the ordinary case of a PEM pasted into the config; it does not cover a key built from an import, an environment variable, or a concatenation.
When the answer cannot be established — a code config the literal could not be read from, a capacitor.config.json that is not valid JSON, a publicKey that is not a string, or a file that could not be read at all — the check reports that it could not be performed, and says so rather than reporting a clean result:
⚠ Could not read the signing key pinned in src-capacitor/capacitor.config.ts:
⚠ it is code, not data, and no `publicKey` string literal holding a public key could be read from it.
⚠ So this is not a clean bill of health. If that config pins a `publicKey`, a different
⚠ key here is rejected by every installed app, permanently and unfixably from the server.
⚠ Check `plugins.DeployYourApp.publicKey` in it yourself before shipping a key change.
That is a warning, not a refusal: a project whose config is a .ts file has to be able to generate its first keypair like any other. It is printed by dya keys generate and by dya deploy; check the file yourself before shipping a key change. Note also that setup only rewrites a .json config — for .ts and .js it prints a snippet for you to paste, so the publicKey in such a config only changes when you change it.
dya init requires authentication
This is a behaviour change. dya init used to write a config file offline; as an alias of dya setup it now needs credentials and a reachable server, and it needs a terminal on both stdin and stdout.
CI is unaffected: .dya/config.json is committed, so a pipeline checks out a project that is already linked and runs dya deploy directly. Nothing in a pipeline should run init or setup. See CI/CD integration.
Generated init module
The plugin's whole required integration is one call — notifyAppReady(), or the plugin rolls the update back after appReadyTimeout. Everything else comes from capacitor.config.json. So the generated module is small; what setup adds is putting it in the right place for your framework.
| Framework | Detected by | Module written | Registration |
|---|---|---|---|
| Quasar | quasar.config.js / .ts |
src/boot/dya.{js,ts} |
'dya' appended to the boot: [] array in quasar.config.* |
| Vue CLI | vue.config.js |
src/dya.js |
import './dya' in src/main.js |
| Create React App | react-scripts dependency |
src/dya.js |
import './dya' in src/index.{js,jsx,tsx} |
| Angular | angular.json |
src/dya.ts |
import './dya' in src/main.ts |
| Svelte | svelte.config.js |
src/dya.ts |
import './dya' in src/main.ts |
| Vite + React | vite.config.* and react |
src/dya.{js,ts} |
import './dya' in src/main.{jsx,tsx} |
| Vite + Vue | vite.config.* and vue |
src/dya.{js,ts} |
import './dya' in src/main.{js,ts} |
| None detected | — | src/dya.js, else dya.js at the web root |
printed instructions only |
The first match in that order wins — a Quasar project also has a Vite config and vue in its dependencies, so the specific signals are checked first. Where the table shows {js,ts}, the extension follows whether your web root has a tsconfig.json; the other rows are fixed, because Angular and Svelte projects are always TypeScript and Vue CLI and Create React App entry points are always JavaScript.
Quasar gets a boot file rather than an edit to App.vue because Quasar generates its own main.ts, and boot files are the framework's designated place for startup side effects:
// src/boot/dya.ts — generated
import { defineBoot } from '#q-app/wrappers';
import { DeployYourApp } from '@deploy-your-app/capacitor-update-manager';
export default defineBoot(async () => {
try {
// Required: without this the plugin rolls back the update after
// appReadyTimeout. Call it once the app has actually booted.
await DeployYourApp.notifyAppReady();
} catch (err) {
// Never let this stop the app from booting: a boot file that throws
// aborts the whole chain. Reaches here when the native bridge is
// absent (plugin not synced into the platform yet), or when the call
// was refused with READY_TOKEN_MISMATCH — which means this bundle is
// about to roll back, and the log is the only warning you get.
console.warn('[DeployYourApp] notifyAppReady() failed:', err);
}
});
Quasar moved the boot wrappers from quasar/wrappers to the #q-app/wrappers alias and renamed boot to defineBoot in @quasar/app-vite v2 and @quasar/app-webpack v4. Setup emits whichever form your project uses:
| Project | Emitted |
|---|---|
@quasar/app-vite v2+, or @quasar/app-webpack v4+ |
import { defineBoot } from '#q-app/wrappers' |
@quasar/app-vite v1, or @quasar/app-webpack v3 and older |
import { boot } from 'quasar/wrappers' |
Neither package pinned in package.json |
whichever your quasar.config.ts/.js imports — #q-app/wrappers if it mentions that alias, otherwise quasar/wrappers |
Both forms are no-op typing helpers at runtime, so the older one still works; setup picks the matching one so the generated file does not import a deprecated module into a project that has already moved on.
Every other framework gets the equivalent as a side-effecting module, so registering it is always exactly one import './dya' line.
The file is yours. Setup generates it once and never reformats, reorders, or restructures your code. Edit it freely — add listeners, move the notifyAppReady() call to after your real first paint, whatever your app needs.
Writing the module and editing your entry point are two separate steps, and the edit to a file you own is the one that needs consent. Setup announces exactly what applying the plan would do and waits for a yes:
ℹ Create src/dya.js and add one import to src/main.js.
? Do that now? (Y/n):
The wording tracks what would actually happen — Update <module> (previously generated by `dya setup`) for a regenerated file, Leave <module> as it is (already up to date) for one that already matches, and for Quasar add 'dya' to the boot array in quasar.config.ts in place of the import clause. The import goes after the last top-level import.
- Blocked — no
boot: [ ... ]array, more than one of them, no top-level import to insert after, no entry file, an unreadable file, or any parse doubt: setup saysCannot register the init module automatically: <reason>, writes the module anyway, and prints the exact text to paste with its target file. Registration is left to you. - Declined — nothing is written at all, and the same instructions are printed.
- Already registered, and the module is on disk and current — the one true no-op. Setup prints
Update init module already registered — leaving it aloneand touches nothing. - Already registered, but the module is missing or out of date — the registration outliving the module is what stops the app building, on a dangling
import './dya', so setup writes the file back instead of reporting success and doing nothing. Nothing is asked: the entry file already imports it, so there is no edit to consent to, and the write only ever touches a path setup owns. A deleted module reportsℹ Create src/dya.js — src/main.js already registers it, but the file is missing., echoes the write as+ Createdwith the module's path, and finishes with✔ src/dya.js was registered but missing — written, so the import resolves again. A stale one reports✔ Update init module already registered — its file was brought up to date. If no module can be written at all, setup saysThe init module is registered but not on disk, and cannot be written: <reason>and prints the manual instructions.
A module whose contents already match is left alone, so a second run reports no changes.
Setup never overwrites a file it did not write
The module names in the table above are the preferred ones. If src/dya.js (or src/boot/dya.ts, or whichever the table shows) already exists and setup did not generate it, that file is left exactly as it is and the next free name is used instead. The candidates, in order:
dya, dya-update, dya-ota, dya-update-2, dya-update-3, dya-update-4
Whichever name is taken up is what gets registered — import './dya-update', or 'dya-update' in the Quasar boot: [] array — so the wiring always points at the file that was actually written. Each skipped file is named, so it is never a mystery why the module has an unexpected name:
⚠ src/dya.js exists and `dya setup` did not write it — left untouched.
The name is then settled for good. A run that landed on dya-update because src/dya.js was yours keeps using dya-update on every later run — even once src/dya.js is gone — because re-choosing from the top of the list would write a second module and register it next to the first, and notifyAppReady() would run twice. A registered name is only reused when its file is one setup may write; a registration pointing at a file you own is your own import, not a previous run's.
Ownership is decided by a marker comment at the top of every generated module:
// @dya-generated — written by `dya setup` (DeployYourApp OTA updates).
// `dya setup` only ever rewrites files carrying this marker. Delete the line
// above to take ownership of this file and the CLI will leave it alone.
Delete that first line and the file becomes yours; setup will route around it on the next run rather than rewriting it. Modules written by earlier CLI versions are recognised too — by the older // Generated by `dya setup`. header, or by being byte-identical to what this version would generate — so an upgrade does not suddenly produce a dya-update.js next to your existing dya.js.
If every candidate name is taken by a file setup did not write, nothing is written at all — no module, and no edit to your entry point, because an import pointing at a file that was never created is a broken build. Setup prints the reason and what to do:
⚠ Cannot register the init module automatically: every candidate module filename is taken
by a file `dya setup` did not generate (src/dya.js, src/dya-update.js, …)
Because nothing was written, the instructions that follow carry the module's whole contents inline — there is no generated file to point you at — and every filename in them is the placeholder NAME rather than one of the files setup just refused to touch:
src/dya.js, src/dya-update.js already exist;
`dya setup` did not write them, so nothing was written at all — the CLI
never overwrites a file it did not generate. Those files are unchanged.
Either move or rename one of them and run `dya setup` again, or add the module
by hand under a name of your own. Replace NAME below with that name:
1. Create src/NAME.js with these contents:
import { DeployYourApp } from '@deploy-your-app/capacitor-update-manager';
void (async () => {
try {
// Required: without this the plugin rolls back the update after
// appReadyTimeout. Call it once the app has actually booted.
await DeployYourApp.notifyAppReady();
} catch (err) {
// The plugin ships a web stub, so this resolves in a browser dev build.
// The guard is for the native bridge being absent (plugin not synced
// into the platform yet) — that must never stop the app from booting.
console.warn('[DeployYourApp] notifyAppReady() failed:', err);
}
})();
2. Register it by adding this line to src/main.js, immediately
after the last import:
import './NAME';
For Quasar the second step is 2. Register it by adding 'NAME' to the boot array in quasar.config.ts: followed by boot: ['NAME'],, and the contents are the boot-file form. The pasted module carries no @dya-generated marker: a file you create by hand was not written by dya setup, and the header must not claim otherwise.
Deploy and rollback
dya deploy
Bundles the web assets, refuses any file that is not a web asset, embeds a per-file hash manifest, checksums and signs the result, uploads it, and points a channel at it.
| Option | Type | Default | Description |
|---|---|---|---|
--channel <channel> |
string | channel from .dya/config.json, else production |
Channel to deploy to. |
--version <version> |
string | Your package.json version plus a UTC build stamp |
Bundle version. See Bundle versions come from package.json. |
--rollout <percent> |
integer | 100 |
Percentage of devices eligible for the update. 0 is honored. Validated by the CLI: must be a whole number from 0 to 100. Anything else — abc, 50.5, 50%, 101, -1 — is rejected before any work happens, with error: option '--rollout <percent>' argument '…' is invalid. Rollout must be a whole number between 0 and 100. and exit 1. |
--message <msg> |
string | — | Release notes, stored on the bundle. Maximum 5000 characters. |
--no-deploy |
flag | Deploys | Upload the bundle without pointing any channel at it. |
--allow-older |
flag | Guards | Publish even when the version is older than the channel's active bundle. See Deploy refuses to move a channel backwards. |
--allow-unsigned |
flag | Refuses | Publish without a signature. Every current client refuses to install the result anyway. See Deploying with no signing key. |
--path <dir> |
string | path from .dya/config.json, else auto-detected |
Directory of built web assets. |
--target <platform> |
string | capacitor |
capacitor or electron. |
npm run build
dya deploy --channel production --message "Fix checkout validation"
ℹ Assets: /home/ada/shop/dist/spa
✔ App: Shop (com.example.shop)
ℹ Version: 1.4.0+20260804.143022
ℹ Channel: production
ℹ Platform: capacitor
✔ 42 files to bundle
✔ All files are web assets
✔ Archive: 3.4 MB
✔ Signed with RSA-4096
✔ Uploaded: 1.4.0+20260804.143022 (3.4 MB)
✔ Deployed to production at 100% rollout
✓ Deploy complete! Version 1.4.0+20260804.143022 is live.
More examples:
# Gradual rollout
dya deploy --channel production --rollout 25
# Upload without releasing; deploy it later from the dashboard
dya deploy --no-deploy --version 1.4.0
# Electron build
dya deploy --target electron --channel beta --path dist/electron
Deploying requires an active, unblocked plan
Every deploy and rollback needs the organization to have an active plan, and every deploy additionally needs the organization's monthly active users and bandwidth to still be within their allowance. The server enforces this, not the CLI — there is no local check and no flag to skip it, because the CLI cannot know your organization's live plan or usage state without asking. A refusal is a 402 from the server: the CLI prints error.message verbatim, the same way it prints any other server error, and exits 1.
No active plan — the trial ended, the subscription lapsed, a payment failed, or a comp expired:
✗ Your Launch trial ended on 14 Jul 2026. Choose a plan to deploy updates again — https://app.deployyour.app/dashboard/settings/billing
A deploy failing this way is not the only thing that has stopped: an organization with no active plan is not served on the device path either, so the apps already installed on your users' phones and desktops are receiving nothing until a plan is active. Nothing is deleted, and both resume together.
Monthly active users or bandwidth blocked — usage went more than 10% over the plan limit and the usage period has not yet rolled over. This is the same block that stops your already-installed apps from receiving updates, so a dya deploy failing this way means devices are not getting anything right now either — see Usage and limits:
✗ Monthly active users limit exceeded (1,142 of 1,000, including a 10% grace). Your devices are no longer receiving updates and new deploys are paused. This resets on 1 Sep 2026, or lifts immediately on a larger plan — https://app.deployyour.app/dashboard/settings/billing
Uploading a bundle (the step before a deploy actually releases it) is refused only by the storage limit or an inactive plan — an MAU/bandwidth block does not stop the upload itself, only the release and the update it would have reached. dya deploy --no-deploy can therefore still succeed while dya deploy (which also creates the deployment) cannot, until the block lifts. See Plans and pricing for the full table of what blocks and what does not.
Bundle versions come from package.json
With no --version, the bundle version is the version in your project's web-root package.json plus a UTC build stamp, joined as semver build metadata:
1.4.0 + 20260804.143022 → 1.4.0+20260804.143022
↑ ↑
package.json UTC YYYYMMDD.HHMMSS
Build metadata is ignored when versions are compared, and that is the point of it. A device running the native 1.4.0 from the App Store compares equal to 1.4.0+<any stamp>, so a web-only redeploy at the same app version still reaches it; 1.3.9+<any stamp> is correctly older and the update check refuses it; 1.4.1+<any stamp> is newer and is served. The stamp keeps each upload unique, because the server rejects a second bundle at a version it already has.
Keep package.json equal to the version you ship to the stores — the Android versionName and the iOS MARKETING_VERSION — and the bundle version matches the store version by construction rather than by discipline. Most release scripts already set all three from one place.
The stamp is UTC, not local time, so two machines in different timezones deploying the same app produce stamps that sort against each other.
A version that cannot be read is fatal. If package.json is missing, unreadable, has no version, or has one that is not a bare x.y.z, the deploy stops before anything is built and exits 1, naming the file and what it found:
✗ /home/ada/shop/package.json has version "1.4", which is not a bare major.minor.patch. Bundle
versions are compared against the native version your app reports, so the version here must be
exactly the number the stores show — or pass --version explicitly.
There is deliberately no fallback to a generated timestamp. Versions like 2026.0731.142530 — which is what this command produced before CLI 0.4.0 — outrank 1.4.0, 2.0.0 and every real release the app will ever make, permanently, and no server-side change undoes it once devices have taken one.
--version still wins outright and is unvalidated by this rule, so a one-off dya deploy --version 1.4.0 behaves exactly as it always did.
Upgrading from CLI 0.3.x. If a channel is currently serving a
YYYY.MMDD.HHMMSSversion, the firstpackage.json-derived deploy is lower than it and the guard below will say so. That is accurate and expected — answer yes once, or pass--allow-older, and the channel is on real version numbers from then on.
Deploy refuses to move a channel backwards
Before anything is zipped or uploaded, dya deploy reads the target channel's active bundle. If the version about to be published is lower, it prints both and asks:
⚠ This deploy would move production backwards.
1.4.0+20260801.090000 (2026-08-01 09:00:00 UTC) ← what production serves now
1.3.9+20260804.143022 (2026-08-04 14:30:22 UTC) ← what this deploy would publish
⚠ The channel would start advertising the older release. Devices already on the newer
⚠ bundle keep it — the update check refuses a downgrade unless the channel has
⚠ allowDowngrade set — so they would simply stop receiving updates until a higher
⚠ version is published.
ℹ Pass --allow-older to skip this check, in CI or when the older bundle is the point.
? Publish this older bundle anyway? (y/N):
Answering no exits 1 with ✗ Deploy cancelled. Nothing was uploaded. — no bundle is uploaded, so there is nothing to clean up afterwards.
Without a TTY the answer is no. A pipeline that has been pointed at the wrong build must not publish it merely because nobody was there to say no. Pass --allow-older to mean it in a script; it skips the check entirely.
Three things are not refusals:
- Equal precedence.
1.4.0+newstampover1.4.0+oldstampis the ordinary web-only hotfix and passes silently — build metadata is not part of the comparison. - A channel with no active bundle, or one that does not exist yet. There is nothing to compare against; a missing channel is still reported after the upload, as before.
- A version neither side can parse. Bundles predating this version scheme carry hand-typed versions, and an unreadable comparison is not evidence of a downgrade.
With --no-deploy the check does not run at all: an upload that points no channel at anything cannot move one backwards.
Unlike the signing-key refusal below, this one has a flag. Publishing an older bundle is recoverable — deploy a newer one, or roll the channel forward — and there are legitimate reasons to want it, which is exactly what allowDowngrade on the channel is for.
Pipeline order
The order matters, because the server and the device both verify the exact bytes that were uploaded:
- Walk the assets directory and hash every file. This is a local pre-flight check — it reports the file count, fails the deploy if the directory holds no files, and warns if there is no root
index.html. These hashes are reused in step 3 to build the manifest; they are not themselves uploaded as a separate artifact. - Check the bundle asset policy. Every file is checked against the allowed-extension list, and every file's leading bytes are checked against a table of native-executable magic numbers (Mach-O, ELF, Android DEX, PE/
MZ, and the Java-class/Mach-O-fatCAFEBABEprefix) regardless of what it is named. Any violation fails the deploy before anything is zipped or uploaded — see Bundle asset policy below. - ZIP the directory, excluding any
node_modules,.git, or.DS_Storepath segment, and write a.dya-manifest.jsoninto the archive root: a JSON document with a schema version, the bundle version, a creation timestamp, and every file's path mapped to its SHA-256 hash (from step 1). Installed clients re-derive each file's hash on extraction and compare it against this manifest — bidirectionally, so a file added or removed after the manifest was written is rejected as much as one that was altered. - Checksum the ZIP bytes (including the manifest entry) with SHA-256.
- Sign those same bytes with RSA-4096 (SHA-256), if the project has a
dya-signing-private.pem. - Upload the bytes plus the metadata.
- Create a deployment on the target channel, unless
--no-deploywas passed.
There is no bundle encryption. Earlier CLI versions optionally encrypted the archive with a dya-encryption-public.pem; that step has been removed outright — an encrypted bundle is opaque to App Store review, and the redesign instead makes the pipeline transparent: a reviewer (or anyone else) can unzip an uploaded bundle and see exactly what ships. See Bundle asset policy below.
The signing key is looked up in .dya/ first, then at the web root, then at the Capacitor root, and finally under its legacy filename, .deployyourapp-private.pem. See Where keys are looked up.
Bundle asset policy
dya deploy refuses to upload a bundle containing anything other than a web asset. Two checks run against every file in the assets directory, before the ZIP is built:
- Extension allowlist. Only these extensions (matched case-insensitively on the final
.) are accepted:html htm css js mjs cjs json map webmanifest svg png jpg jpeg gif webp avif ico bmp woff woff2 ttf otf eot mp3 mp4 webm ogg wav m4a txt xml csv md wasm. A file with no extension, or any extension not on this list, is rejected — including a native library or binary renamed to look like one of the above. - Executable magic bytes. Regardless of extension, a file whose leading bytes match a known native-executable format is rejected: Mach-O (
FE ED FA CE,FE ED FA CF,CE FA ED FE,CF FA ED FE), a Mach-O fat binary or Java class (CA FE BA BE), ELF (7F 45 4C 46), Android DEX (64 65 78 0A), or a PE/DOS executable (4D 5A). This is what stops a.dylibrenamed toapp.jsfrom shipping.
A violation reports every offending file at once, not just the first, and the deploy exits 1 before anything is zipped or uploaded:
✗ This build contains 2 file(s) that an OTA bundle may not carry:
- "libnative.dylib" has a file type that OTA bundles may not contain: .dylib. Bundles may only contain web assets.
- "sneaky.js" is a Mach-O binary. OTA bundles may only contain web assets, whatever the file is named.
OTA bundles may only contain web assets. Remove these files from your build output, or exclude them from the directory you deploy.
The server performs a lighter check at upload, by name only: it rejects a disallowed file extension and requires a .dya-manifest.json entry to be present, both without decompressing any entry — content inspection was removed server-side deliberately, because decompressing an untrusted archive on a shared box is an OOM vector. It does not read magic bytes and does not parse the manifest, so it cannot perform a hash check of any kind. The extension check, the magic-byte check, and the bidirectional manifest verification are all fully enforced only on device, during extraction — that is the load-bearing check this design relies on. The CLI check above exists so an obvious violation is caught at the developer's terminal instead of at upload or, worse, silently on a device.
Deploy refuses to sign with a key your apps reject
Before anything is signed, dya deploy fingerprints the signing key it would use and compares it with the publicKey pinned in the Capacitor config — the only key an installed app ever verifies against. If they differ, the deploy stops and exits 1 without uploading:
✗ Refusing to deploy: this bundle would be signed with a key your apps reject.
src-capacitor/capacitor.config.json e2bbb0fc4a91d3e7 ← what installed apps verify against
.dya/dya-signing-private.pem 59c5420cb8d7f102 ← what this deploy would sign with
⚠ Every installed app checks updates against e2bbb0fc4a91d3e7. A bundle signed with
⚠ 59c5420cb8d7f102 fails that check, so this release and every release after it would
⚠ be rejected — while the upload succeeds and nothing on the server looks wrong.
ℹ Two ways out, and only one of them is usually right:
ℹ • Get the private key for e2bbb0fc4a91d3e7 — whoever made the first release has it —
ℹ and put it in .dya/dya-signing-private.pem. The Capacitor config is committed;
ℹ `.dya/*` is not, which is why a fresh clone lands exactly here.
ℹ • Or, only if no build carrying e2bbb0fc4a91d3e7 was ever installed anywhere, re-pin
ℹ the app with `dya setup` — it asks for that in words — and rebuild for the stores.
⚠ There is no flag for this. A signed bundle your apps reject cannot be undone.
There is deliberately no override flag. A --force on the publishing command would be pasted into a CI script exactly once and would then disable the guard permanently, for every deploy after it, with nobody ever reading the warning again. A legitimate key rotation has a route that does not need one: dya setup takes the typed consent, re-pins the Capacitor config to the new key, and this comparison then passes honestly — because the two fingerprints really do match. See Key rotation.
Two cases are not refusals by this specific guard. An unsigned deploy is not what this guard checks for — it only compares fingerprints when a signing key is actually about to be used — but unsigned is not a free pass either: see Deploying with no signing key for the separate refusal that catches it. And a Capacitor config the pinned key could not be read from is reported as unreadable, not as clean — see Reading the pinned key out of a .ts or .js config. If the signing key itself cannot be read as a private key, the CLI warns and lets signing fail with its own error rather than mislabeling the cause.
Deploying with no signing key
With no dya-signing-private.pem anywhere in the project, dya deploy warns No signing key found. An unsigned bundle is refused by every installed client — nothing will install it. and what happens next depends on what the Capacitor config pins:
- Nothing pinned (or the config would not say): you are offered
Generate RSA-4096 signing + encryption keys now?. It defaults to yes, except after a "could not be read" report, where it defaults to no. Without a TTY the answer is no, and the offer being declined — interactively or by the non-TTY default — is what leads into the refusal below. - A key already pinned: the offer is withdrawn entirely, not merely warned about. Generating here would mint a different keypair, and every current client refuses any bundle it cannot verify — so a bundle signed with a new, unpinned key is rejected by every installed app exactly as permanently as an unsigned bundle is refused by all of them. Neither option installs anything; generating a new keypair here would not be an improvement, so the CLI does not offer it as one:
⚠ Not offering to generate keys here. Neither option installs: an unsigned bundle is
⚠ refused by every current client, and a bundle signed with a NEW key is rejected by
⚠ every app that already pins the old one, permanently.
ℹ Put the private key for e2bbb0fc4a91d3e7 in .dya/dya-signing-private.pem to sign this release.
Either way, with still no signing key resolved, dya deploy refuses to upload an unsigned bundle by default and exits 1:
✗ Refusing to deploy an unsigned bundle.
⚠ Every installed client now refuses a bundle with no signature — this deploy would upload
⚠ an artifact that can never install, on any client, anywhere.
ℹ Two ways out:
ℹ • Run `dya keys generate` (or restore an existing key into .dya/) and sign this release.
ℹ • Pass --allow-unsigned if you genuinely intend an artifact no current client will accept —
ℹ e.g. a first deploy before any client build exists, or a CI run with no key available.
--allow-unsigned is the only way past this, and there is no prompt to answer instead — no answer to "are you sure?" changes whether the resulting bundle installs, so the flag has to be named explicitly. With it, the deploy proceeds exactly as it always did for an unsigned bundle: the server still rejects the upload outright for any app that already has a signing public key registered; for an app with none, the upload succeeds, but no installed client will ever apply the result, because signature verification is mandatory on every current platform. There is no configuration, client-side or server-side, in which an unsigned bundle runs — --allow-unsigned produces an upload, not a working update.
The two legitimate reasons to reach for it: a first deploy for an app with no client build installed anywhere yet, where nothing exists to reject the bundle; and a CI pipeline that wants the rest of its steps (upload, channel wiring) exercised before signing keys are provisioned.
Deploy behavior and failure modes
Version format. With no
--version, the version is yourpackage.jsonversion plus a UTCYYYYMMDD.HHMMSSbuild stamp —1.4.0+20260804.143022. The stamp has second granularity, so back-to-back CI deploys do not collide. Versions are unique per app, across platforms — the same full version string cannot be uploaded twice for one app even with different--targetvalues, though two bundles at the same app version with different stamps are fine. See Bundle versions come from package.json.Asset detection. When neither
--pathnor a configuredpathis given, the CLI checks thewebDirdeclared incapacitor.config.*first, then<capacitor root>/www, thendist/spa,dist/pwa,dist,www,build,publicunder the web root. It accepts only a directory containing anindex.html, so a declaredwebDirthat has not been built yet falls through to a real build rather than winning empty. Finding nothing exits1.webDircomes first because it is the only authoritative answer — it is the directorynpx cap synccopies into the native app, so whatever is there is what ships. It matters most in a split layout: Quasar in Capacitor mode builds tosrc-capacitor/www, which is nowhere under the web root.Empty or wrong assets directory.
--pathis taken at face value — it skips theindex.htmlcheck that auto-detection applies. If the directory contains no files at all the deploy stops before uploading anything and exits1; if it has files but no rootindex.html, the CLI warns and continues.A file that is not a web asset. An extension not on the allowlist, or a file whose leading bytes match a native-executable format regardless of its name, fails the deploy before anything is zipped or uploaded, listing every offending file. See Bundle asset policy.
Unsigned bundles. With no signing key,
dya deployrefuses to upload by default and exits1.--allow-unsignedoverrides it and the deploy proceeds unsigned, but the result still cannot install anywhere — every current client refuses a bundle it cannot verify. See Deploying with no signing key.A signing key that is not the pinned one. Refused before anything is uploaded, exit
1, with no flag to override. See Deploy refuses to sign with a key your apps reject.An unreadable
package.jsonversion. Refused before anything is built, exit1, unless--versionwas passed. See Bundle versions come from package.json.A version older than the channel's active bundle. Prompted before anything is uploaded; no on a non-TTY.
--allow-olderskips it. See Deploy refuses to move a channel backwards.Missing channel. The channel is looked up before the upload — the backwards guard needs its active bundle — but a channel that does not exist is still only reported afterwards, so the bundle is already on the server when the deploy step fails and exits
1. Create the channel withdya channels create --name <name>, then release the uploaded bundle from the dashboard — re-runningdya deploywith the same version is rejected as a duplicate.No active plan, or a metered limit blocked past its grace. A
402from the server, exit1, witherror.messageprinted as-is. See Deploying requires an active, unblocked plan.Two-factor uploads. If the organization requires a two-factor code for uploads, the CLI prompts for a 6-digit code and retries the upload. This needs a TTY, so it cannot be satisfied in a pipeline.
Server limits. Bundles are capped at 500 MB, and uploads are rate limited to 30 per hour.
dya rollback
Points a channel back at a previous bundle. Without --bundle the server picks the deployment before the current one; with no previous deployment the command fails. Rollbacks always deploy at 100% rollout.
| Option | Type | Default | Description |
|---|---|---|---|
--channel <channel> |
string | channel from .dya/config.json, else production |
Channel to roll back. Resolved exactly the way dya deploy resolves it. |
--bundle <bundleId> |
string | Previous bundle | Roll back to this specific bundle ID instead. |
dya rollback and dya deploy pick the channel the same way — flag, then the project config, then production — so a bare dya rollback always undoes what a bare dya deploy shipped. The channel is printed before any network request, so you can see what is about to be rolled back:
# In a project whose .dya/config.json says "channel": "beta"
dya rollback
ℹ Channel: beta
✔ App: Shop
✔ Rolled back beta
✓ Rollback complete
Pass the flag to override the project config:
dya rollback --channel production
ℹ Channel: production
✔ App: Shop
✔ Rolled back production
✓ Rollback complete
A bundle the retention policy has expired — its storage object reclaimed because it fell out of every channel's 5 most-recent set and passed 15 days old, see Bundle retention — cannot be rolled back to, whether it is the automatically-picked previous bundle or named explicitly with --bundle. The command exits 1 and prints a message naming the retention policy rather than "not found": the bundle's record still exists, only its file is gone.
App commands
dya apps list
Lists the apps in the active organization. Takes no options.
dya apps list
Apps (1):
Shop (com.example.shop)
Platform: both Channels: 2 Bundles: 12 Devices: 340
dya apps create
Creates an app in the active organization. A production channel is created with it and marked as the default.
| Option | Type | Default | Description |
|---|---|---|---|
--name <name> |
string | required | Display name, 1–100 characters. |
--app-id <appId> |
string | required | Bundle/app identifier. Must start with a letter and contain only letters, numbers, ., _, or -. |
--platform <platform> |
string | both |
One of ios, android, both (Capacitor), or electron. An app targets one bundle family — create a separate app to ship desktop. |
dya apps create --name "Shop" --app-id com.example.shop --platform both
✔ Created app: Shop
App IDs are globally unique across the whole platform, not just your organization. Reusing one that is already registered fails with a conflict.
Channel commands
Channel commands resolve the app from the appId in .dya/config.json, so run them from inside the project.
dya channels list
Lists the channels for the current app with the bundle each one currently serves. Takes no options.
dya channels list
Channels for Shop:
production [default] → 1.4.0+20260804.143022
beta → no bundle
dya channels create
| Option | Type | Default | Description |
|---|---|---|---|
--name <name> |
string | required | Channel name, 1–50 characters, starting with a letter and containing only letters, numbers, -, or _. |
dya channels create --name beta
✔ Created channel: beta
dya channels delete
Deletes a channel by name. The default channel cannot be deleted; the server rejects that with a permission error.
| Option | Type | Default | Description |
|---|---|---|---|
--name <name> |
string | required | Channel to delete. |
dya channels delete --name beta
✔ Deleted channel: beta
Key commands
RSA private keys are generated locally and never leave your machine. Only the two public keys are sent to the server. The encryption private key currently has no consumer — dya setup does not offer to embed it anywhere — but it is still never uploaded.
dya keys generate
Generates two RSA-4096 key pairs into .dya/ at your project's web root, then uploads the public halves to the server when an organization and app can be resolved.
| Option | Type | Default | Description |
|---|---|---|---|
--app-id <appId> |
string | appId from .dya/config.json |
App to upload the public keys to. |
| File | Written with | Purpose |
|---|---|---|
.dya/dya-signing-private.pem |
mode 0600 |
Signs bundles during dya deploy. Never leaves your machine, never commit it. |
.dya/dya-signing-public.pem |
default mode | Uploaded to the server for signature verification. |
.dya/dya-encryption-public.pem |
default mode | Uploaded to the server. dya deploy does not use it — bundles are never encrypted; see Pipeline order. |
.dya/dya-encryption-private.pem |
mode 0600 |
Kept locally, never uploaded. Not read by dya deploy — bundles are never encrypted, so this key currently has no consumer; do not embed it in your app. |
dya keys generate
✓ Added to .gitignore: .dya/*, !.dya/config.json
✔ Generated RSA-4096 key pairs
ℹ Signing keys generated:
ℹ Private key: /home/ada/shop/.dya/dya-signing-private.pem (keep secret, use to sign bundles)
ℹ Public key: /home/ada/shop/.dya/dya-signing-public.pem (uploaded to the server)
ℹ Encryption keys generated:
ℹ Public key: /home/ada/shop/.dya/dya-encryption-public.pem (uploaded to the server)
ℹ Private key: /home/ada/shop/.dya/dya-encryption-private.pem (kept locally, never uploaded)
⚠ dya deploy no longer encrypts bundles — every archive is a plain, signed ZIP, checked
⚠ against the bundle asset policy so App Store review can see exactly what ships. This
⚠ keypair currently has no consumer: do not embed the private key in your app, and do
⚠ not wire it into a plugin config expecting it to decrypt anything.
⚠ Back these private keys up somewhere safe — a password manager or secrets vault.
✔ Signing + encryption public keys uploaded to server
The .gitignore block is written before the keys exist, not after, so an interrupted run cannot leave a private key that the next git add -A would stage. Commit it alongside .dya/config.json. When the block is already present the line reads ℹ Already in .gitignore: .dya/*, !.dya/config.json instead.
Writing the lines and git honouring them are different questions, so afterwards the CLI puts .dya/config.json to real git check-ignore. Anything still ignoring it is reported with the file, line and pattern responsible — including a rule in a file the CLI must not edit, such as a parent directory's .gitignore or your global excludes file. See The rule is checked against real git.
When git cannot be asked at all, the success line above is qualified rather than left to imply a verdict nobody gave:
✓ Added to .gitignore: .dya/*, !.dya/config.json
⚠ Written, but not verified: git could not say whether .dya/config.json is still ignored.
⚠ That happens when git is not on PATH, this is not a git repository, or git did not answer in time.
⚠ A rule elsewhere may still be hiding it, which would leave CI with no appId or channel.
⚠ Check it once git is available: git check-ignore -v .dya/config.json
The lines are in the file either way — the caveat is about the check, not the write. Run the printed command yourself once git is available.
The keys are written whether or not the upload works. If no organization or app ID can be resolved — you are not logged in yet, or the project has no config — it prints Log in and set an active org (and app ID) to upload keys to the server. and skips the upload. If the upload itself fails it warns with the server's message and prints Upload them later with: dya keys upload --app-id <appId>.
One keypair per project
The command refuses to generate when a signing private key already exists anywhere in the project — in .dya/, at the web root, at the Capacitor root, or under a legacy filename. It prints Key pairs already exist. Delete the existing keys to regenerate., lists all four key paths (or missing), re-applies the .gitignore block, and tells you to run dya keys upload to re-upload the public halves. Delete the existing key files if you genuinely intend to replace them, and read Key rotation first, because rotation needs a native release.
If it finds two distinct signing keypairs, it prints each one's path and public-key fingerprint, marks the one whose fingerprint matches the publicKey embedded in capacitor.config.json, and exits 1 without writing anything. There is no prompt: the CLI never picks for you, because choosing the key that is not the embedded one is exactly the failure that makes every installed app reject that update and every update after it. Delete the one you do not want and re-run. See One keypair per app.
Generating over a key that already shipped
Both checks above look only at private keys on disk, and the case that matters most has none of them. capacitor.config.json is committed while .dya/* is gitignored, so a fresh clone of a shipped project has the pinned public key and no private half at all — neither guard fires, and what dya keys generate would mint is a keypair every installed app rejects.
So when the Capacitor config pins a different publicKey, generating requires the same typed phrase dya setup uses. The consequences are printed first (the same block shown under Setup will not mint a keypair over a pinned one, since both commands say this in one vocabulary), then:
? Type "replace the signing key" to generate a DIFFERENT keypair anyway, or press Enter to stop:
The answer is trimmed and compared case-insensitively. Anything else — a bare Enter, a near miss — writes nothing and exits 1:
ℹ No keys were generated. The app stays pinned to e2bbb0fc4a91d3e7.
Typing it generates the keypair and says what has to happen next:
⚠ Generating a keypair that is not e2bbb0fc4a91d3e7, on your say-so.
⚠ Run `dya setup` afterwards to embed the new key, then ship a new store build with it
⚠ before you deploy — every app carrying the old key is unreachable by OTA update.
Without a terminal there is nobody to ask, so the command refuses and exits 1 rather than defaulting either way:
✗ No keys were generated: this needs a terminal, and there is nobody here to ask.
ℹ Put the private key for e2bbb0fc4a91d3e7 in .dya/dya-signing-private.pem,
ℹ or run `dya keys generate` in a terminal if that key really is expendable.
When dya setup runs the generator it passes the consent it has already taken for that same fingerprint, so the phrase is only ever asked once per run. And when the pinned key cannot be read at all, this is a warning rather than a refusal — a project has to be able to make its first keypair. See Reading the pinned key out of a .ts or .js config.
dya keys upload
Reads dya-signing-public.pem and, when present, dya-encryption-public.pem from the project and uploads them. Use this when the keys were generated on another machine, or when the upload during dya keys generate was skipped.
| Option | Type | Default | Description |
|---|---|---|---|
--app-id <appId> |
string | appId from .dya/config.json |
App to upload the public keys to. |
dya keys upload --app-id com.example.shop
✔ Signing + encryption public keys uploaded
✓ Keys uploaded
Exits 1 if the project has no signing public key, or if no organization and app ID can be resolved.
Organization commands
dya orgs list
Lists the organizations you belong to and marks the active one. Takes no options.
dya orgs list
Organizations (2):
Acme Inc (acme) ← active
Role: owner Members: 4 Apps: 3 Plan: Scale
Side Project (side-project)
Role: owner Members: 1 Apps: 1 Plan: Launch
dya orgs switch
Sets the active organization in the global config.
| Option | Type | Default | Description |
|---|---|---|---|
--slug <slug> |
string | — | Organization slug, or its exact name. An unknown value exits 1. |
dya orgs switch --slug acme
✓ Switched to: Acme Inc (acme)
Without --slug: a single-organization account has its one organization selected without a picker; a multi-organization account gets a numbered picker on a TTY, and in a non-interactive shell the command prints Multiple organizations found. Use: dya orgs switch --slug <slug> and exits 1.
Single-organization accounts rarely need this command, because the other commands auto-select the only organization and cache it.