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

@otakit/capacitor-updater

v2.3.1

Published

Capacitor plugin for OTA updates

Readme

@otakit/capacitor-updater

Capacitor OTA updater plugin for OtaKit.

What it does

  • fetches the latest manifest for its release lane from the CDN
  • downloads and verifies OTA bundles
  • stages updates safely
  • applies staged bundles on cold start, resume, or manual command
  • uses notifyAppReady() as the health handshake
  • rolls back automatically if a newly applied bundle does not prove healthy

The plugin is intentionally small:

  • three automatic lifecycle entry points: runtime, launch, resume
  • one shared set of update primitives: check, download, apply
  • one rollback safety loop

There is no built-in splash or overlay manager in this model.

Hosted config

plugins: {
  OtaKit: {
    appId: "YOUR_OTAKIT_APP_ID",
    appReadyTimeout: 10000,

    // Optional:
    // channel: "staging",
    // runtimeVersion: "2026.04",
    // launchPolicy: "apply-staged",
    // resumePolicy: "shadow",
    // runtimePolicy: "immediate",
    // checkInterval: 600000,
  }
}

Advanced overrides for self-hosting or custom trust only:

  • cdnUrl
  • ingestUrl
  • serverUrl
  • manifestKeys
  • allowInsecureUrls

Hosted OtaKit already points at the managed ingest service and CDN and already trusts the managed manifest signing keys.

Policies

The plugin has three automatic events:

  • runtime Cold start where the current runtimeVersion lane has not been resolved yet.
  • launch Normal cold start after runtime is already resolved.
  • resume App returning from background.

Each event uses the same policy names:

type OtaKitPolicy = 'off' | 'shadow' | 'apply-staged' | 'immediate';

Semantics:

  • shadow check + download, never apply on that event
  • apply-staged apply a staged bundle if one already exists, otherwise behave like shadow
  • immediate check + download + apply

Recommended defaults:

launchPolicy = 'apply-staged';
resumePolicy = 'shadow';
runtimePolicy = 'immediate';

That means:

  • fresh install or new runtimeVersion: catch up immediately
  • later cold starts: apply already staged content if present, otherwise download the next update in the background
  • resumes: periodically check and stage in the background

If an app wants full JS control:

launchPolicy = 'off';
resumePolicy = 'off';
runtimePolicy = 'off';

Force-immediate releases

A release (or revert) can be marked force immediate in the dashboard or with otakit release --force-immediate. The flag is baked into the signed manifest; when a device's automatic flow sees it, shadow and apply-staged events escalate to the immediate behavior for that release: download, apply, and reload on that event.

Bounds to keep in mind:

  • It is "immediate on the next event/check", not push — delivery is bounded by lifecycle events and checkInterval (static CDN, no server→device channel).
  • off policies never fetch a manifest, so they never see the flag. off stays the device-owned kill switch, and the manual APIs are unchanged.
  • Trial and rollback still apply: a forced bundle that never calls notifyAppReady() rolls back like any other.
  • It reloads the app under the user, possibly mid-task. Use it for broken releases, not routine rollouts.

Check interval

checkInterval defaults to 10 minutes and only applies to background resume checks:

  • resumePolicy: "shadow"
  • resumePolicy: "apply-staged" when there is no staged bundle to apply

Set checkInterval to 0 or a negative value to disable resume throttling.

It does not throttle:

  • launch handling
  • runtime handling
  • immediate
  • manual JS APIs

Runtime model

The plugin keeps three important pointers:

  • current the bundle the WebView is serving now
  • fallback the last known-good bundle used for rollback
  • staged a downloaded bundle waiting to be activated

Bundle lifecycle:

download -> pending -> trial -> success
                        |
                        +-> error -> rollback

If a bundle is applied and never calls notifyAppReady():

  • timeout triggers rollback while the app is running
  • or the next cold start detects the still-trial bundle and rolls back before boot continues

The last failed applied bundle is persisted so the plugin does not immediately download and apply the same broken release again.

On top of this per-device rollback, a release published with the auto-revert flag is reverted fleet-wide by the server when too many devices report rollbacks within a 24-hour window, so remaining devices never download the broken bundle.

Automatic flow

For the normal hosted path, most apps only need:

await OtaKit.notifyAppReady();

The plugin handles checking, staging, applying, rollback, and runtime-lane catch-up based on the configured policies.

Loading screen recommendation

OtaKit does not manage a splash screen or loading overlay for you.

Recommended startup order:

  1. keep a native splash screen or fullscreen loading view visible
  2. finish your normal app bootstrap
  3. call notifyAppReady()
  4. hide the splash screen or loading view

For React and Next.js apps, treat this as part of the default setup, not an optional polish step.

Manual APIs

The manual surface maps to the same internal engine:

const state = await OtaKit.getState();
const check = await OtaKit.check();
const download = await OtaKit.download();
await OtaKit.notifyAppReady();

check() returns:

type CheckResult =
  | { kind: 'no_update' }
  | { kind: 'already_staged'; latest: LatestVersion }
  | { kind: 'update_available'; latest: LatestVersion };

download() returns:

type DownloadResult = { kind: 'no_update' } | { kind: 'staged'; bundle: BundleInfo };

update() uses the same native immediate-flow operation as automatic "immediate" policies:

await OtaKit.update();

That keeps the manual convenience path atomic inside the native plugin instead of splitting it into separate download() and apply() calls.

apply() and successful update() are terminal operations:

  • on success they reload the WebView
  • they do not resolve back into the old JS context
  • call notifyAppReady() from normal startup after the reloaded app boots

Events

The plugin emits lifecycle events alongside the pull APIs:

OtaKit.addListener('updateAvailable', (latest) => {}); // newer bundle found, before download
OtaKit.addListener('updateStaged', ({ bundle }) => {}); // downloaded + verified + staged
OtaKit.addListener('updateApplied', ({ bundle }) => {}); // new bundle confirmed healthy
OtaKit.addListener('downloadFailed', (failure) => {}); // non-terminal download/verify error
OtaKit.addListener('rollback', (failure) => {}); // applied bundle reverted (notify timeout)
OtaKit.removeAllListeners();

Events fire only while the app process is alive — there is no buffering or replay. Reconcile on startup:

  • a bundle staged in a previous session: getState().staged
  • a startup rollback (app restarted before notifyAppReady()): it happens before any JS runs, so it never reaches a listener — check getLastFailure()

apply() reloads the WebView and destroys the JS context, so updateApplied fires in the reloaded bundle. Attach updateApplied/rollback listeners early in app startup, not in the restart click handler.

Download and stage are atomic in this plugin — there is a single updateStaged event, not separate "downloaded" and "staged" events.

The headline pattern — background download via shadow policies, prompt to restart:

const state = await OtaKit.getState();
if (state.staged) showRestartPrompt(state.staged);
OtaKit.addListener('updateStaged', ({ bundle }) => showRestartPrompt(bundle));

Example manual flow

const check = await OtaKit.check();

if (check.kind === 'update_available') {
  const result = await OtaKit.download();
  if (result.kind === 'staged') {
    await OtaKit.apply();
  }
}

Or use the one-shot helper:

await OtaKit.update();

After the app reloads and starts again, call:

await OtaKit.notifyAppReady();

Runtime channel switching

setChannel() overrides the configured channel at runtime — for example a "Join beta" toggle in settings — without rebuilding the app:

// Opt into the beta channel; takes effect on the next check/download cycle.
await OtaKit.setChannel({ channel: 'beta' });

// Back to the channel from capacitor.config.ts (or the base channel).
await OtaKit.setChannel({ channel: null });

const { channel, source } = await OtaKit.getChannel();
// source is 'override' after setChannel(), 'config' otherwise

The override is persisted across launches and slots into channel resolution as: explicit call argument → persisted override → config channel → base. setChannel() itself never checks, downloads, or reloads anything — call OtaKit.download() / OtaKit.update() afterwards if you want the switch to take effect immediately.

Channel names must match ^[A-Za-z0-9._-]{1,64}$, must not contain .., and must not be the reserved names base or default (matching server-side validation). Invalid names reject without persisting.

Limitations to be aware of:

  • Channels are public CDN paths. A channel name is not a secret and this cannot enforce private distribution — anyone who guesses the name can fetch that channel's manifest.
  • The backend is not aware of the switch. There is no server-side record of which device is on which channel; the switch is purely client-side.
  • The channel must exist and have a release. Switching to a channel with no published manifest yields no_update until something is released there.

Compatibility lanes

  • channel answers "who should get this rollout?"
  • runtimeVersion answers "which native app shell can safely run this bundle?"

Use channels for rollout tracks such as beta, staging, or production.

Use runtimeVersion when a new store build creates a new compatibility boundary and you do not want devices on that native shell to keep receiving older OTA bundles.

Trust model

The plugin does not just download arbitrary zips from a URL.

  1. it fetches the latest manifest for the current app + channel + runtime lane
  2. it verifies the manifest signature when keys are configured
  3. it compares the manifest with current, staged, and last-failed local state
  4. it downloads only when a newer usable bundle exists
  5. it verifies the downloaded object against the manifest sha256
  6. if the bundle is encrypted, it decrypts it (AES-256-GCM; the tag authenticates the plaintext)
  7. it stages and later applies the bundle

Bundle encryption (optional)

Bundles can be end-to-end encrypted so that object storage and the CDN only ever hold ciphertext. Generate a key with otakit generate-encryption-key, put it in CI as OTAKIT_ENCRYPTION_KEY (the CLI then encrypts uploads), and ship the same key in the app:

plugins: {
  OtaKit: {
    // Inject from an env var at build time — never commit the key.
    bundleKeys: [{ kid: process.env.OTAKIT_ENCRYPTION_KID!, key: process.env.OTAKIT_ENCRYPTION_KEY! }],
  },
},

Per upload the CLI generates a random data key (DEK), encrypts the zip with AES-256-GCM, and wraps the DEK under your app key; the wrapped DEK and nonces travel in the signed manifest. The server never sees the key and cannot decrypt your bundles.

Threat model — confidentiality, not DRM. The decryption key ships inside the app binary, so a determined attacker who reverse-engineers the app can extract it — true of every client-side encryption scheme. What it protects against: leaked or guessed CDN URLs, bucket misconfiguration, and casual inspection of stored objects. Update forgery is independently blocked by the ES256 manifest signature — which is also why encryption should only be used with manifest signing enabled (hosted default): the encryption parameters are covered by the signature.

Operational rules:

  • Rollout order: ship a store build containing bundleKeys before releasing encrypted bundles. Installed apps without the key cannot decrypt and will stay on their current version (they keep running; the failure is recorded as download_error telemetry and, if you listen for it, the downloadFailed event — it does not appear in getLastFailure(), which reports rollbacks only).
  • Rotation: bundleKeys is an array — ship old + new keys together during a transition, then drop the old key in a later store build.
  • Key custody: back the key up. Losing it means installed apps cannot receive updates until a store build ships a new key.
  • Decryption failures are never fatal: they behave like download failures — the running bundle is untouched and nothing unverified is ever applied.
  • Memory: decryption buffers the bundle in memory (roughly 2–3× the bundle size at peak). Keep encrypted bundles comfortably under ~100 MB, especially for low-end Android devices.

Source areas

  • src/definitions.ts: public types and config
  • src/index.ts: Capacitor registration and JS wrapper
  • src/web.ts: web fallback implementation
  • ios/Sources/UpdaterPlugin/*: iOS implementation
  • android/src/main/java/com/otakit/updater/*: Android implementation