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.5.1

Published

Capacitor OTA live updates for iOS and Android — ship JS/HTML/CSS without an app store review. Signed bundles, update channels, staged rollouts, auto-rollback, and analytics, powered by DeployYourApp (https://deployyour.app).

Readme

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

Over-the-air (OTA) live updates for Capacitor apps. Ship JavaScript, HTML, and CSS changes to installed iOS and Android apps without going through app store review — signed bundles, update channels, percentage rollouts, automatic rollback, and update analytics.

🌐 deployyour.app

Website · Documentation · Pricing · Compare OTA platforms · Changelog · Dashboard

npm license


What this is

This is the Capacitor client SDK for DeployYourApp, a hosted OTA update, distribution, and diagnostics platform for Capacitor and Electron apps. The plugin checks for updates, downloads and verifies signed bundles, activates them, and rolls back automatically when a bundle fails to start. The server side — apps, channels, rollouts, devices, and analytics — lives in your DeployYourApp dashboard, so the plugin needs an account (every plan starts with a 30-day trial; see pricing).

Use it when you want to:

  • Push a JavaScript/CSS bugfix to users the same day, instead of waiting on an app store review cycle — "hot code push" / live update for Capacitor
  • Run staging and production update channels off one binary
  • Roll a release out to a percentage of devices, then roll it back instantly if something breaks
  • Distribute internal corporate apps and keep them current
  • See what version each device is actually running

The rest of the platform:

| Package | What it is | |---------|-----------| | @deploy-your-app/cli | dya — sets this plugin up, then bundles, signs, and deploys your updates | | @deploy-your-app/electron-update-manager | The same live-update pipeline for Electron desktop apps |

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.

publicKey is required. Signature verification is mandatory and has no fail-open path: without a public key the plugin refuses every download with SIGNATURE_REQUIRED. Generate the pair with dya keys generate; dya setup writes the public half into your Capacitor config for you.

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();

If your web sources cannot resolve that bare import — a Quasar/Capacitor project keeps the plugin in src-capacitor/node_modules, which the web bundler does not see — use Capacitor's own seam and get the identical API:

import { registerPlugin } from '@capacitor/core';
const DeployYourApp = registerPlugin('DeployYourApp');

Nothing in this package's JavaScript is load-bearing: every guarantee below holds at the native bridge boundary, so both forms behave the same. (Before 0.5.0 that was not true — see Migrating to 0.5.0.)

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 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 () => {
  try {
    await DeployYourApp.notifyAppReady();
  } catch (err) {
    // A boot file that throws aborts Quasar's whole boot chain.
    console.warn('[DeployYourApp] notifyAppReady() failed:', err);
  }
});

#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,
  });

  // 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 | required | RSA public key (PEM) for bundle signature verification. Without it every download fails with SIGNATURE_REQUIRED | | 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) | | 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, policy-check, and store a bundle | | apply(options) | Activate a downloaded bundle and reload the webview ({ id }) | | notifyAppReady(options?) | Confirm the bundle loaded — prevents rollback. Rejects READY_TOKEN_MISMATCH if the calling document is not the bundle on trial | | 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: { bundle: BundleInfo \| 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 | | appReadyRejected | { bundleId, servedBundleId, message } | notifyAppReady() was refused because the calling document could not be shown to be running the bundle on trial; the call also rejects with READY_TOKEN_MISMATCH | | appVersionChange | { previousVersion, currentVersion } | Native app version changed; reset to built-in (resetWhenUpdate) |

The events that can legitimately land during launch, before your listener code has run, are retained by Capacitor and delivered to the first listener that attaches: updateAvailable, downloadComplete, downloadFailed, updateApplied, updateFailed, rollback, appReadyRejected, and appVersionChange. Both platforms retain exactly these.

The remaining three — downloadProgress, noUpdateAvailable, and appReady — are delivered live and 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_REQUIRED, SIGNATURE_INVALID, BUNDLE_NOT_FOUND, INVALID_URL, EXTRACTION_FAILED, ASSET_TYPE_REJECTED, MANIFEST_MISSING, MANIFEST_MISMATCH, READY_TOKEN_MISMATCH, STORAGE_ERROR, UNKNOWN.

| Code | Meaning | |------|---------| | SIGNATURE_REQUIRED | publicKey is not configured, or the server sent no signature. There is no unsigned install path | | ASSET_TYPE_REJECTED | A bundle entry is not a web asset — its extension is outside the allowlist, or its leading bytes are a native executable's | | MANIFEST_MISSING | The bundle carries no readable .dya-manifest.json, or one whose manifestVersion is not 1. Re-deploy with a current dya CLI | | MANIFEST_MISMATCH | The extracted files and the manifest disagree: a wrong hash, a file with no manifest entry, a manifest entry with no file, or a files map that is absent or malformed in a manifest that otherwise parsed |

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 over the raw ZIP, before the archive is opened. Both are mandatory
  4. Extract under policy — Unzip into the app's bundle storage (Zip-Slip protected, size-capped), rejecting any file that is not a web asset
  5. Manifest check — Every extracted file must match .dya-manifest.json, in both directions
  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.

No hidden or debug surface

This plugin installs no gesture recognizers and no touch listeners, and it exposes no hidden or undocumented entry point. Every capability it has is a documented method or event in the tables above.

If your app wants a debug affordance — a hidden gesture to reach a diagnostics screen, for example — implement it in your own app code, where you control whether it ships and can document it for App Review. It does not belong in an update plugin that every install embeds. See App Store review.

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

Disabling analytics stops analytics events and /api/stats requests. It does not stop update checks: every /api/update request still includes the persistent deviceId so the service can select and deliver updates for that installation.

// 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. The iOS SDK privacy manifest declares linked, non-tracking Device ID for App Functionality and Analytics, plus linked, non-tracking Product Interaction, Performance Data, and Other Diagnostic Data for Analytics. The latter categories cover default lifecycle durations, byte counts, and failure reasons. Those declarations describe this SDK's collection; they do not replace your host app's privacy manifest or App Store Connect answers. Keep those answers consistent with the SDK configuration and every other data flow in your shipped app. 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 is mandatory and 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). There is no fail-open path: no publicKey, no update
  • Bundles are not encrypted. They are plain, signed ZIPs, so anyone holding a bundle URL — App Review included — can see exactly what a bundle contains. See Bundle asset policy
  • ZIP extraction is Zip-Slip protected with per-file (100 MB) and total (500 MB) size caps; symlink entries are skipped on iOS and neutralised on Android (see Bundle asset policy)
  • Signing private keys never leave developer machines

Bundle asset policy

The native extractor on both platforms limits published bundles to allowlisted web-asset paths and rejects known executable headers before publication. See App Store review.

1. Extension allowlist. Every file entry must end in one of:

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

Only the final extension counts, so bundle.js.map is allowed and evil.js.dylib is not. A file with no extension is rejected. Directory entries are exempt — they have no extension, and requiring one would reject every nested folder.

Symlink entries differ by platform. On iOS they are skipped and never written. On Android they cannot be identified: a ZIP records symlink-ness in the central directory's external attributes, and java.util.zip.ZipEntry exposes no accessor for it. Such an entry is therefore extracted as an ordinary file whose content is the link target path — never as a link, so it cannot redirect a later write out of the bundle directory. It must then pass the extension allowlist and the manifest hash check like any other file, and a manifest generated from a real build tree does not list it, so it is rejected rather than installed.

2. Executable magic-byte rejection. The first four bytes of every extracted file are checked regardless of its name, so a native binary renamed to app.js is still refused:

| Leading bytes | Format | |---------------|--------| | FE ED FA CE | Mach-O | | FE ED FA CF | Mach-O | | CE FA ED FE | Mach-O | | CF FA ED FE | Mach-O | | CA FE BA BE | Mach-O fat binary or Java class | | BE BA FE CA | Mach-O fat binary | | CA FE BA BF | 64-bit Mach-O fat binary | | BF BA FE CA | 64-bit Mach-O fat binary | | 7F 45 4C 46 | ELF | | 64 65 78 0A | Android DEX | | 4D 5A | PE executable |

3. Per-file manifest. Every bundle carries a .dya-manifest.json at its root, written by dya deploy:

{
  "manifestVersion": 1,
  "bundleVersion": "1.4.0",
  "createdAt": "2026-08-04T00:00:00.000Z",
  "files": { "index.html": "<sha256 hex>", "assets/app.js": "<sha256 hex>" }
}

The check is bidirectional: every manifest entry must exist on disk with the declared hash, and every extracted file must appear in the manifest. A one-way check would let an attacker add a file or remove one. The manifest itself is the only file exempt from appearing in files — it cannot list its own hash.

The CLI excludes a source .dya-manifest.json and source symlinks from both hashing and archiving, then writes exactly one generated manifest after the directory walk. The ZIP's non-directory file entries are therefore exactly the manifest files set plus that generated manifest; the upload service rejects archives with anything other than one root manifest.

manifestVersion is validated: anything other than 1 is rejected with MANIFEST_MISSING, naming the version found and the version expected. A manifest the app cannot interpret is treated as one it does not have.

A bundle produced by an older CLI has no manifest and is rejected with MANIFEST_MISSING. There is no fallback path; re-deploy with a current dya.

These byte and filename checks are limited technical evidence about which files can be published. They cannot guarantee App Store compliance or determine whether web code changes reviewed functionality. Apply the operational release boundary in the App Review guidance, and keep store metadata, review notes, and privacy answers accurate for every available release.

Native release validation

The repository's Native release validation workflow runs the Android JUnit and iOS XCTest suites against exact Capacitor 6, 7, and 8 releases. Each matrix entry packs this npm package, installs that tarball into a clean Capacitor consumer, syncs the native platform, and compiles a representative app. The iOS gate also checks the built App.app for PrivacyInfo.xcprivacy.

Run the same smoke gates on a machine with the corresponding native toolchain:

pnpm test:native:android -- 6.2.1
pnpm test:native:ios -- 6.2.1

The scripts reject versions outside the supported Capacitor 6-8 range.

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

For Windows, macOS, and Linux desktop apps, use the sibling package @deploy-your-app/electron-update-manager — same signed-bundle pipeline, same dashboard, same dya deploy.


Links

| | | |---|---| | 🌐 Website | https://deployyour.app | | 📚 Documentation | https://deployyour.app/docsplugin setup, configuration, update lifecycle, App Store review | | 💰 Pricing | https://deployyour.app/pricing — 30-day trial on every plan | | 🔍 Compare | https://deployyour.app/compare | | 📊 Dashboard | https://app.deployyour.app | | 📝 Changelog | https://deployyour.app/changelog | | ✉️ Support | [email protected] |

Built and maintained by DeployYourApp. MIT licensed.