npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@deploy-your-app/capacitor-update-manager

v0.2.1

Published

Live update and analytics plugin for CapacitorJS apps — part of the DeployYourApp platform

Readme

@deploy-your-app/capacitor-update-manager

Capacitor native plugin for live OTA updates, analytics, and corporate app shell features, powered by DeployYourApp.

Installation

npm install @deploy-your-app/capacitor-update-manager
npx cap sync

Requires Capacitor 6+. No extra native setup is needed — CocoaPods / Gradle pick everything up from npx cap sync (the iOS pod pulls in ZIPFoundation automatically).

Tip: if you use the DeployYourApp CLI, dya setup installs and configures this plugin for you interactively — including generating the init module below and registering it in your app's entry point. It never overwrites a source file it did not write: if the filename it wants is already yours, it picks the next free one (dya-update, dya-ota, …), registers that instead, and keeps using that name on later runs. The publicKey it writes into capacitor.config.json is derived from the signing private key dya deploy uses, and setup refuses to write anything if the two disagree — or if a different public key is already embedded here, which is what a fresh clone of a shipped project looks like. Replacing that one takes typing replace the signing key; pressing Enter keeps it and stops setup.

Quick Start

1. Configure (capacitor.config.js)

export default {
  appId: 'com.yourcompany.yourapp',
  plugins: {
    DeployYourApp: {
      appId: 'your-app-id',                    // from the DeployYourApp dashboard
      channel: 'production',
      publicKey: '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----',
    },
  },
};

That is the whole configuration — the plugin talks to deployyour.app by default.

updateUrl / statsUrl only need setting to point the app at a different DeployYourApp API — a local server during development, say. They are base URLs (e.g. http://localhost:3000); the plugin appends /api/update and /api/stats itself. A non-default API also needs its bundle storage host listed in allowedDownloadHosts.

2. Confirm each successful launch

import { DeployYourApp } from '@deploy-your-app/capacitor-update-manager';

// Call once your app has finished booting. If this is not called within
// `appReadyTimeout` after an update, the plugin rolls back automatically.
await DeployYourApp.notifyAppReady();

That's the whole integration. With the default autoUpdate: true, the plugin checks for updates on launch (and every checkInterval seconds), downloads new bundles in the background, verifies + decrypts them, and activates them on the next app launch.

Quasar

Quasar generates its own main.ts, so the call belongs in a boot file — the framework's designated place for startup side effects — rather than in App.vue.

// src/boot/dya.ts
import { defineBoot } from '#q-app/wrappers';
import { DeployYourApp } from '@deploy-your-app/capacitor-update-manager';

export default defineBoot(async () => {
  await DeployYourApp.notifyAppReady();
});

#q-app/wrappers and defineBoot are the current names, introduced in @quasar/app-vite v2 and @quasar/app-webpack v4. On older versions use the previous path instead — import { boot } from 'quasar/wrappers' and export default boot(...). Copy whichever form your own quasar.config file uses; dya setup detects it from your package.json (falling back to what the config imports) and generates the matching file.

Then register it in quasar.config.ts:

boot: ['dya'],

A Quasar project keeps the Capacitor project in src-capacitor/, which has its own package.json. Install this plugin in both — the web root so the import above resolves, and src-capacitor/ so npx cap sync picks up the native code. dya setup writes the boot file, the boot: [] entry, and both installs for you. If you already have a src/boot/dya.ts of your own, setup leaves it untouched and registers the boot file it did write under another name.

Other frameworks

Anywhere else, a side-effecting module imported once from your entry file does the same job:

// src/dya.js
import { DeployYourApp } from '@deploy-your-app/capacitor-update-manager';

DeployYourApp.notifyAppReady();
// src/main.js — one line, after your other imports
import './dya';

Manual update flow (optional)

Set autoUpdate: false to drive updates yourself:

const update = await DeployYourApp.checkForUpdate();
if (update.available) {
  const { id } = await DeployYourApp.download({
    url: update.url,
    version: update.version,
    checksum: update.checksum,
    signature: update.signature,
    sessionKey: update.sessionKey,
  });

  // Activates the bundle and reloads the webview immediately.
  await DeployYourApp.apply({ id });
}

Configuration

| Option | Type | Default | Description | |--------|------|---------|-------------| | appId | string | required | Your app ID from DeployYourApp | | updateUrl | string | https://api.deployyour.app | Update server base URL (no path — /api/update is appended) | | statsUrl | string | https://api.deployyour.app | Analytics server base URL (no path — /api/stats is appended) | | allowedDownloadHosts | string[] | ['storage.deployyour.app'] | Extra hosts allowed to serve bundle downloads. The updateUrl and statsUrl hosts are always allowed. Bundles are fetched over https only | | channel | string | 'production' | Update channel (persisted when changed via setChannel()) | | autoUpdate | boolean | true | Check + download automatically; activate per applyMode | | applyMode | string | 'whenIdle' | When auto-updates activate: 'whenIdle' / 'onLaunch' (next launch), 'immediate' (reload now), 'background' (download only, you call apply()). The server can override per-update; mandatory updates always apply immediately. | | appReadyTimeout | number | 10000 | Ms to wait for notifyAppReady() before auto-rollback | | checkInterval | number | 600 | Seconds between automatic update checks (0 disables repeat checks) | | publicKey | string | — | RSA public key (PEM) for bundle signature verification | | encryptionPrivateKey | string | — | RSA private key (PEM) for E2E bundle decryption, embedded in the app binary | | analyticsEnabled | boolean | true | Enable batched analytics | | analyticsBatchSize | number | 20 | Events before auto-flush | | analyticsFlushInterval | number | 30 | Seconds between auto-flush | | autoDeleteFailed | boolean | true | Auto-delete failed bundles | | autoDeletePrevious | boolean | true | Auto-delete old bundles after a successful update | | resetWhenUpdate | boolean | true | Reset to the built-in bundle when the native app version changes (store update) | | enableTestGesture | boolean | false | Enable three-finger tap gesture (for corporate shell) | | directUpdate | string | — | Deprecated. Legacy alias for applyMode |

API Reference

Update Lifecycle

| Method | Description | |--------|-------------| | checkForUpdate(options?) | Check server for available updates ({ channel? }) | | download(options) | Download, verify, decrypt, and store a bundle | | apply(options) | Activate a downloaded bundle and reload the webview ({ id }) | | notifyAppReady() | Confirm the bundle loaded — prevents rollback | | reset() | Delete all downloaded bundles and revert to the built-in bundle | | reload() | Force reload the webview |

Bundle Management

| Method | Description | |--------|-------------| | getCurrentBundle() | Get active bundle info ({ id: 'builtin', ... } when none) | | getNextBundle() | Get the bundle staged for next launch, or null | | listBundles() | List all downloaded bundles | | deleteBundle({ id }) | Remove a stored bundle |

Channel Management

| Method | Description | |--------|-------------| | getChannel() | Get current update channel | | setChannel({ channel }) | Switch update channel (persisted; used by the next check) |

Device Identity

| Method | Description | |--------|-------------| | getDeviceId() | Get stable device UUID | | setCustomId({ customId }) | Set a custom device identifier (e.g. employee ID) |

Analytics

| Method | Description | |--------|-------------| | trackEvent({ name, properties? }) | Track a custom event | | trackPageView({ path, title? }) | Track a page view | | trackError({ message, stack?, fatal? }) | Track an error | | flushAnalytics() | Flush buffered events immediately |

eventData is stored verbatim — see Data collected by this SDK before putting anything user-derived in it.

Version Info

| Method | Description | |--------|-------------| | getNativeVersion() | Get the native app version | | getPluginVersion() | Get the plugin version | | setVersionOverride({ version }) | Report a fake version to the update server (testing). Empty string clears it | | getVersionOverride() | Get the active version override ('' when unset) |

Events

import { DeployYourApp, DYA_EVENTS } from '@deploy-your-app/capacitor-update-manager';

DeployYourApp.addListener(DYA_EVENTS.DOWNLOAD_PROGRESS, (data) => {
  console.log(`Download: ${data.percent}%`);
});

DeployYourApp.addListener(DYA_EVENTS.UPDATE_AVAILABLE, (data) => {
  console.log(`Update ${data.version} available`);
});

| Event | Data | Description | |-------|------|-------------| | downloadProgress | { percent, bytesDownloaded, totalBytes } | Download progress (throttled to whole-percent changes) | | updateAvailable | { version, message? } | A check found a new update | | noUpdateAvailable | — | A check found nothing new | | downloadComplete | { id, version } | Download finished (manual or auto) | | downloadFailed | { message } | Download or auto-update error | | updateApplied | { id, version } | Bundle activated | | updateFailed | { message } | apply() failed | | rollback | { from, to, reason, attempts? } | Auto-rollback triggered (reason: 'appReadyTimeout' or 'crashLoop'; attempts is the launch count for 'crashLoop') | | appReady | — | notifyAppReady() confirmed | | appVersionChange | { previousVersion, currentVersion } | Native app version changed; reset to built-in (resetWhenUpdate) | | testGestureTrigger | { timestamp } | Three-finger tap detected |

Only two events are retained by Capacitor and delivered to a listener that attaches after they fire: updateAvailable and rollback — the two that can legitimately land during launch, before your listener code has run. Both platforms retain exactly these two.

Every other event in the table above is delivered live and is lost if nothing is listening at that moment. Register your listeners before calling checkForUpdate() (or before notifyAppReady(), for appReady) if you depend on them.

Error Codes

Rejected calls carry a stable code you can branch on:

MISSING_PARAMS, NETWORK_ERROR, PARSE_ERROR, DOWNLOAD_FAILED, CHECKSUM_MISMATCH, SIGNATURE_INVALID, BUNDLE_NOT_FOUND, INVALID_URL, EXTRACTION_FAILED, DECRYPTION_FAILED, STORAGE_ERROR, UNKNOWN.

try {
  await DeployYourApp.download(update);
} catch (err) {
  if (err.code === 'CHECKSUM_MISMATCH') {
    // corrupted download — retry
  }
}

Update Flow

  1. CheckPOST {updateUrl}/api/update with app ID, device ID, platform, versions
  2. Download — Fetch the bundle ZIP with progress events
  3. Verify — SHA-256 checksum + RSA signature validation (when publicKey is configured)
  4. Decrypt — AES-256-GCM session key unwrapped with the RSA private key from encryptionPrivateKey (E2E mode only)
  5. Extract — Unzip into the app's bundle storage (Zip-Slip protected, size-capped)
  6. Apply — Point the Capacitor webview at the new bundle and reload (immediately via apply() / applyMode: 'immediate', or on the next launch otherwise)
  7. Ready checknotifyAppReady() must be called within appReadyTimeout
  8. Rollback — Auto-reverts to the previous bundle (or built-in) if the ready check never arrives

A bundle that crashes the app before appReadyTimeout can elapse would never trip the timer at all. A persisted launch counter covers that case: after 3 consecutive launches without notifyAppReady(), the bundle is rolled back and a rollback event with reason: 'crashLoop' is emitted.

Only one download runs at a time, and the currently active bundle can never be re-downloaded over itself. Version strings are validated against ^[0-9A-Za-z.\-+]{1,64}$ before being used as directory names, and each bundle is extracted to a staging directory that is swapped into place only after it is verified to contain a servable index.html.

Corporate App Shell

When enableTestGesture is enabled, a three-finger tap triggers the testGestureTrigger event. This is used by the DeployYourApp corporate app shell to return from a full-screen corporate app back to the main shell.

  • iOS: UITapGestureRecognizer with numberOfTouchesRequired = 3
  • Android: OnTouchListener detecting 3 pointers within 500 ms

Data collected by this SDK

You are the data controller for your end users. This SDK sends data to DeployYourApp on your behalf, and you must disclose it in your own privacy policy. Full detail, including app store questionnaire guidance, is at https://deployyour.app/privacy.

Sent on every update check (POST /api/update) and stored

| Field | Notes | |-------|-------| | deviceId | Random UUID v4 generated by the plugin on first run and stored locally. Not an IDFA, GAID, ANDROID_ID, MAC address, or hardware serial, and not derived from one. | | platform | ios or android | | nativeVersion | Version of the installed app binary | | pluginVersion | This plugin's version | | first seen / last seen | Set server-side on each check-in | | channel subscriptions | Which update channels this device follows |

Transmitted but discarded by the server. The Android implementation also sends osVersion, the device manufacturer and model, customId, currentBundleId, and nativeBuild. The server validates against a strict schema that excludes these, so they are stripped and never stored. Do not rely on them being available in the dashboard.

Never collected: geolocation, advertising IDs, contacts, photos, phone number, IMEI, installed-app lists, or your app's own data.

Analytics (POST /api/stats) — on by default

analyticsEnabled defaults to true. Events are buffered and flushed every 20 events or 30 seconds, and each stores eventType, eventData, bundleVersion, timestamp, and the device ID.

The plugin emits its own events without you calling anything. While analyticsEnabled is true, both platforms send these as the update lifecycle runs:

update_check, update_available, update_download_start, update_download_complete, update_download_fail, update_verify_pass, update_verify_fail, update_apply, update_app_ready, update_rollback

These carry update metadata only — bundle version, size, duration, error code — never end-user data. Your own calls to trackEvent(), trackPageView(), and trackError() are sent on the same channel. eventData is arbitrary JSON stored verbatim: whatever your app puts in it, we store. A stack trace passed to trackError() containing a user's email means that email is stored. Audit your call sites.

Turning it off

// capacitor.config.js
export default {
  plugins: {
    DeployYourApp: {
      appId: 'your-app-id',
      analyticsEnabled: false, // no events sent; update checks still work
    },
  },
};

To stop contacting our servers entirely, also set autoUpdate: false and checkInterval: 0 and never call checkForUpdate().

On-device storage

The plugin persists the device ID, selected channel, custom ID, and version override in UserDefaults (iOS) / SharedPreferences (Android), and under the dya_* localStorage keys on web. In the EU/UK, storing an identifier on a user's device engages ePrivacy consent rules; update delivery is a plausible strict-necessity case, analytics generally is not.

What you need to disclose

Name DeployYourApp as a processor; describe the device identifier and the technical version fields; describe your own analytics events if you leave analytics on; state your legal basis and retention. For store privacy labels the baseline is Device or Other ID, collected and linked to the installation, used for App Functionality — not tracking. Retention, deletion, and export are covered in the linked doc; note that per-device deletion is not self-serve yet and goes through [email protected].

Security

  • Bundles are downloaded over https only, from an allow-listed host — the updateUrl/statsUrl hosts plus allowedDownloadHosts. Plain http is accepted only from localhost, for local development. Rejections carry code: 'INVALID_URL'.
  • Bundle integrity verified via SHA-256 checksum
  • RSA signature verification ensures bundles came from your build pipeline (the CLI generates RSA-4096 keys; both platforms derive the modulus size from the key itself, so any RSA key size is accepted)
  • Optional AES-256-GCM end-to-end encryption ("DYA1" format) — the decryption key ships only inside your compiled app
  • ZIP extraction is Zip-Slip protected with per-file (100 MB) and total (500 MB) size caps; symlinks are skipped
  • Signing/encryption private keys never leave developer machines

Platform Support

  • iOS — Swift implementation (iOS 13+, ZIPFoundation for extraction)
  • Android — Kotlin implementation
  • Web — Partial: analytics, channel/device identity, and version override work (backed by localStorage); update download/apply are unavailable and checkForUpdate() always reports no update