WP Manifestindependent plugin directory
manifest / updater

The Manifest updater

Dashboard → Updates, served from your own forge. One JSON file, one release zip, a short class you own. No WordPress.org listing, no middleman, no license server.

Most plugin authors assume Dashboard → Updates only works for plugins on WordPress.org. It does not. WordPress asks a few filters "is there a newer version, and where is the package?" Answer those from a manifest.json in your repository and a zip attached to your release, and sites get the same Updates screen, the same one-click install, and the same "View details" modal they would get from the directory.

This is a convention, not a library. Copy the pattern, rename the class, change what you need. Disembark, Minn Admin, CaptainCore Manager, WP Freighter, WP Shipyard and a handful of other listed plugins ship it today. The 2023 walkthrough is the origin story; this page is the current guideline.

WordPress site your-plugin 1.1.0 update transient (cached about an hour) WordPress.org listing not needed Your forge · GitHub, GitLab, Codeberg… Default branch manifest.json version · download_url · sha256 Release v1.2.0 your-plugin.zip plus any language packs 1 · fetch manifest.json 2 · 1.2.0 is newer, offer it 3 · download zip, verify sha256
The whole loop. The file on the default branch answers "is there a newer version?"; the release asset is only the package.

The three things that matter

  1. Publish a manifest.json on the default branch (main or master) and point the updater at its raw URL. Sites read that file, not the release API.
  2. Attach an installable zip to every release, named like the plugin folder (your-plugin.zip). The forge's auto-generated "Source code (zip)" has the wrong root folder, breaks installs, and does not count as a download here.
  3. Cache the check. Store the remote manifest in a transient for an hour (a day is fine), flush it after a successful update, and never hit the forge on every admin page load.

Everything below is detail, hardening, or optional extras.

What goes in manifest.json

The minimum the updater needs:

{
  "name": "Your Plugin",
  "slug": "your-plugin",
  "author": "<a href=\"https://example.com\">You</a>",
  "version": "1.2.0",
  "download_url": "https://github.com/you/your-plugin/releases/download/v1.2.0/your-plugin.zip",
  "requires": "6.0",
  "tested": "6.9",
  "requires_php": "7.4",
  "homepage": "https://example.com",
  "sections": {
    "description": "One or two sentences for the plugin details modal."
  }
}

Worth adding:

Keep version and download_url in lockstep with the release you just cut. Ship a copy of manifest.json inside the plugin too, so the version display and details modal still work when the forge is unreachable. That bundled copy is a fallback only: it cannot honestly contain its own zip's hash, so sha256 verification always reads the remote file on the default branch.

Where the updater looks

Pin one HTTPS URL to the raw file on the default branch:

https://raw.githubusercontent.com/you/your-plugin/main/manifest.json

Serving the same JSON from your own site also works (WP Freighter started that way), but the default-branch file is simpler, and it is what the WP Manifest crawler recognises as a manifest updater.

The release zip

WP Manifest counts only .zip release assets toward downloads and the active-site estimate. Source archives inflate the numbers without being installs.

What the PHP side does

Three hooks are the whole "library": a short class you can paste and rename.

Hook What the site sees plugins_api reads manifest "View details" modal name · sections · download_link site_transient_update_plugins compares versions Row on the Updates screen response.package = download_url upgrader_process_complete after install Cache purged next check sees the new local version
Each filter maps to one thing the site owner sees. The middle one is the update itself.
  1. plugins_api — when WordPress asks for plugin_information about your slug, return name, version, sections and download_link from the manifest.
  2. site_transient_update_plugins — if the remote version is newer than the installed constant, put a response object in $transient->response with package set to download_url.
  3. upgrader_process_complete — delete your transient so the next check sees the new local version.

Reference implementations: Disembark (app/Updater.php), Minn Admin (includes/class-minn-admin-updater.php), and CaptainCore Manager.

Caching

WordPress reads site_transient_update_plugins constantly: admin screens, cron, the admin-bar badge. Without a cache every read becomes a live request to your forge.

Hardening worth stealing

From the newer implementations (Minn Admin, CaptainCore Manager):

Translations (optional, but native)

You do not need translate.wordpress.org to offer language packs. Put them on the same release and list them in the manifest:

"translations": [
  {
    "language": "de_DE",
    "version": "1.2.0",
    "updated": "2026-09-11 12:00:00",
    "package": "https://github.com/you/your-plugin/releases/download/v1.2.0/your-plugin-de_DE.zip",
    "sha256": "…"
  }
]

Then, in the same site_transient_update_plugins filter, append matching entries to $transient->translations. WordPress installs them into wp-content/languages/plugins/ and refreshes them after a plugin update on its own.

Lessons from Minn Admin:

Cutting a release

1 BumpVersion: 2 Buildyour-plugin.zip 3 Create release,attach the zip 4 Computesha256 5 Updatemanifest.json 6 Push thedefault branch on the release on the default branch
The manifest is updated last, after the zip exists and has a hash. Pushing it is what publishes the release; sites see it on their next cached check.
  1. Bump Version: in the main plugin file (and any mirrored constant).
  2. Build your-plugin.zip with the correct root folder.
  3. Create the forge release (v1.2.0) and attach the zip, plus any language-pack zips.
  4. Compute sha256 of each zip.
  5. Update manifest.json on the default branch: version, download_url, last_updated, sha256, and translations if you ship packs.
  6. Commit and push. Sites pick it up on the next cached check.

Automate steps 4 and 5 once the project grows. Minn Admin's bin/release-manifest.js stamps hashes and translation entries without hand-editing JSON.

Let an agent wire it up

If you work with Cursor, Claude, Copilot or similar, you do not have to write this by hand. Open your plugin repo, paste one of these prompts, and let the agent adapt a real reference implementation. Swap the reference URLs for CaptainCore Manager or WP Shipyard if you prefer those as the template. The agent should read those files and rename the class, slug and cache key for your plugin, not invent a parallel update system.

Baseline — same shape as Disembark

Wire up plugin updates for this plugin the same way Austin Ginder's Disembark plugin does.

Reference: https://github.com/DisembarkHost/disembark
- app/Updater.php
- manifest.json on the default branch
- release zip attached to GitHub Releases (not the auto-generated source archive)

Requirements:
1. Add a root manifest.json with name, slug, version, download_url, requires, tested, requires_php, homepage, and a short sections.description.
2. Add a small Updater class that filters plugins_api and site_transient_update_plugins, caches the remote manifest in a transient, falls back to the local manifest.json if GitHub is unreachable, and purges the cache after a successful update.
3. Point the remote check at the raw manifest.json on this repo's default branch.
4. Document the release steps: bump Version, build an installable {slug}.zip, attach it to the GitHub release, update manifest.json version + download_url, push the default branch.
5. Do not add Composer packages or Plugin Update Checker unless I ask. Keep it a short class I own.

With translations — same shape as Minn Admin

Set up this plugin to update from GitHub, including translations, like Minn Admin.

Reference: https://github.com/austinginder/minn-admin
- includes/class-minn-admin-updater.php
- manifest.json (including translations[] and sha256)
- language packs as separate release assets
- docs/i18n-roadmap.md for the delivery model

Requirements:
1. Manifest-based plugin updates from the raw manifest.json on the default branch, with a cached transient and local fallback.
2. Allowlist package URLs (https, this repo's /releases/download/ path) and require sha256 verification on download.
3. Offer language packs through $transient->translations from manifest.translations[], only for locales the site actually uses.
4. Each language-pack zip should include .po, .mo, and .l10n.php (plus JS JSON if we enqueue scripts), installed to wp-content/languages/plugins/.
5. Stamp sha256 for the plugin zip and each pack into manifest.json as part of the release process; do not trust the copy of the manifest bundled inside the plugin zip for verification.
6. Keep the implementation in-repo (no new SaaS). Match Minn's behaviour, adapted to this plugin's slug and namespace.

How this shows up on WP Manifest

You do not register anything with WP Manifest for updates to work. The directory indexes what you already ship; the updater talks only to your forge.

Related options

The Manifest updater is for authors who want Dashboard → Updates without a middleman and without a large dependency.

Examples

Questions, or a listing that should credit a Manifest updater and does not: feedback@wpmanifest.xyz.