Skip to main content

WP Registry

WP Registry connects an ordinary WordPress installation to an independently hosted plugin and theme registry, including their translations. It works with any theme and does not require a frontend framework. WordPress core updates, checksums and core language packs continue to use WordPress's native services.

The companion PHP/MySQL publisher is in src/wp-registry-server/. It imports the public WordPress.org plugin and theme catalogue in resumable batches, stores verified ZIPs, and publishes local plugins and themes. Client requests for these catalogues and packages never proxy to WordPress.org. Importing upstream content is a separate operator/cron task.

Deployment topology

Public portal: https://wpregistry.studiochampgauche.com. Registry provider: https://area.wpregistry.studiochampgauche.com.

The registry origin serves the API under /v1/, immutable ZIP downloads under /packages/, verified publisher accounts under /publish/, operator moderation under /moderate/, and legacy configured publishers under /manage/. No second download hostname is necessary. The provider URL remains configurable.

Use the server's cPanel deployment guide to create its dedicated account, PHP runtime, private storage, MySQL database, Directory Privacy operator authentication and cron jobs. The complete kit supplies both sibling domain roots: public_html/wpregistry.studiochampgauche.com/ for the public portal and public_html/area.wpregistry.studiochampgauche.com/ for the registry. The application remains in wp-registry-server/, with private data in sibling registry-private/. Configuration, signing material, uploaded ZIPs and working files stay outside public_html. The deployment archive includes a client-template/ directory; the server's build-client command creates registry-private/wp-registry-0.3.0.zip with the provider's public URL and Ed25519 verification key included. The portal home offers this configured plugin directly, preferring a published client release when available.

Installing the configured ZIP and activating it is enough to connect a site. On multisite, activate it network-wide so cron and administration use the same provider. The source checkout deliberately has empty provider values and performs no takeover until configured. Deactivation restores native distribution services and clears old update offers.

For an operator-managed installation, protected WordPress configuration can override the bundled public values:

define('WP_REGISTRY_URL', 'https://area.wpregistry.studiochampgauche.com');
define('WP_REGISTRY_PUBLIC_KEY', 'REPLACE_WITH_PUBLIC_KEY_FROM_REGISTRY_INIT');

The public key must be the canonical Base64 representation of exactly 32 bytes. Never put a private signing key in WordPress, a client ZIP or the document root. Updating the WP Registry plugin itself must preserve the provider configuration; the publisher builds and validates provider-specific client packages.

Client requirements and behavior

  • WordPress 6.0+, PHP 7.4+, Sodium and ZIP extensions, and HTTPS access to the provider.
  • Native installation screens, capabilities, nonces, filesystem transport, upgrade locks, maintenance mode and administrator auto-update choices remain owned by WordPress.
  • The HTTP hook replaces known plugin/theme directory, update and translation requests. It intercepts both native HTTP and HTTPS variants. Unrecognized plugin/theme distribution API versions fail closed.
  • WordPress core version checks, checksums, release archives and core translations retain their native requests, responses and installers, including when the registry is unavailable or misconfigured.
  • WordPress.org package URLs are refused while the provider is configured, including old persisted offers. News, patterns and unrelated services are outside this distribution scope. Independent commercial updaters and local ZIP uploads keep their own native paths.
  • Plugin updates require the exact basename and validated publisher/update identity. A matching slug alone does not authorize taking over an installed extension. Local publications with Update URI: false can be enrolled through exact publisher/adoption identities.
  • The plugin offers Catalogue and Updates action links in the installed-plugins screen.
  • This does not update mu-plugins or authored development source trees. Deployments that replace installed plugin/theme files must keep their source and deployment process synchronized with approved releases.

Replacing Universal Registry with WP Registry 0.3.0

WP Registry has a new plugin identity: wp-registry/init.php. It is a separate installation, not an automatic update or silent adoption of the former universal-registry slug.

  1. Generate the configured wp-registry-0.3.0.zip from the current registry origin and its current public verification key.
  2. Deactivate Universal Registry before activating WP Registry. On multisite, check network activation and every site's active plugins; deactivate the old client everywhere before network-activating the new one.
  3. If WordPress configuration defines the old UNIVERSAL_REGISTRY_URL or UNIVERSAL_REGISTRY_PUBLIC_KEY constants, replace those definitions with WP_REGISTRY_URL and WP_REGISTRY_PUBLIC_KEY, using the current provider values above. The new client does not read the old constants.
  4. Install the new ZIP and activate WP Registry. Verify its plugin/theme catalogue and updates. The old inactive plugin can remain during this check; it does not interfere.

Activation is refused when the old plugin remains active on the current site or network. If both plugins are enabled through another process, WP Registry registers no distribution hooks and displays an administrator notice. It never deactivates another plugin automatically. This guard reads WordPress activation options, so either plugin load order is covered. A network activation cannot inspect every site's active plugins in one request: the runtime guard also applies separately on each site.

The first eligible load also removes the previous client's update_core transient and its obsolete core/checksum/mixed-translation metadata and failure caches if the network-aware ur_distribution_scope marker is below 2. This idempotent migration already existed in 0.2.0; renaming the plugin does not rerun a completed migration. Native WordPress performs subsequent core checks normally. Later registry invalidations clear only plugin and theme update offers. Every monotonic signature revision watermark is preserved, and changing the provider URL or public key selects a separate provider identity without deleting old state. No tables or persistent options are renamed or removed as part of the product rename. The core migration can run with an unconfigured or invalid provider after the conflicting client has been deactivated.

The PHP classes now use the WPRegistry namespace and the wp-registry text domain. The plugin entry explicitly loads its four classes from includes/; it needs no Composer autoloader and installs no alias for the old namespace. The companion server's autoloader separately owns its WPRegistry classes. Metadata routes, schema 1, ur_* persisted keys, ur_identity payload fields and existing universal_registry_* error codes remain compatible. The server's UR_CONFIG and deployment environment variables remain unchanged; they are not client configuration constants.

Authenticated distribution contract

Every metadata operation is POST with a JSON object. The response is {payload, signature}: canonical Base64 encodings of exact signed JSON bytes and an Ed25519 detached signature. Verification uses the locally pinned public key, never a key supplied by the response.

The signed object contains schema: 1, request, revision, issued_at, expires_at, and data. request is SHA-256 of POST, a newline, the exact /v1/... route, a newline, and the exact request-body bytes. Re-encoding the payload before checking its signature is incorrect. Times are integer Unix timestamps, with at most a one-hour lifetime and five minutes of forward clock tolerance.

The client records a monotonic revision in the main site's uniquely indexed options table using an atomic numeric maximum and an uncached comparison. It rejects lower revisions. Each installation also requests a fresh package descriptor with a random nonce in the signed request binding, so a cached offer cannot authorize a withdrawn package.

OperationRouteRequest essentials
Plugin catalogue / detail / tags/v1/plugins/query, /information, /tags under /v1/pluginsquery page/search or exact slug
Theme catalogue / detail / features/v1/themes/query, /information, /features under /v1/themesquery page/search or exact slug
Plugin updates/v1/updates/pluginsbasename map with Version, UpdateURI, AuthorURI; WordPress/PHP versions
Theme updates/v1/updates/themesstylesheet map with the same headers; WordPress/PHP versions
Translations/v1/updates/translationssingular plugin/theme type, slug and version
Installation authorization/v1/packages/informationSHA-256 plus fresh 64-hex-character nonce

The server returns WordPress-compatible data shapes. Plugin/theme update entries additionally carry ur_identity for exact installed-header binding. Catalogue fields are explicitly projected and sanitized before reaching WordPress administration. Arbitrary HTML, external screenshots, SVGs and embedded previews are not accepted as trusted UI.

Packages use only the configured origin and /packages/<sha256>.zip. A signed descriptor binds type, slug, plugin basename where applicable, version, root, size, digest and URL. Translation descriptors additionally bind language and translation type. The client compares updates with the current native offer, downloads with safe HTTP and no redirects, verifies size and SHA-256, and inspects the ZIP before native extraction.

Bounds: 1 MiB request JSON; 4 MiB signed response; 15-second metadata timeout; one cache slot per operation, at most 15 minutes and never past signed expiry; 30-second transport failure backoff. Queries accept up to 100 items per page, page 10,000 and 100 UTF-8 bytes of search text. Archives are limited to 128 MiB compressed, 512 MiB expanded, 30,000 entries and 64 MiB per entry, with expansion-ratio, path, root, duplicate, symlink and file-type checks. Package transfer has a 120-second bound. The web publisher has a smaller 100 MiB upload limit.

Plugin/theme translation ZIPs are restricted to the selected type/slug/locale namespace. PHP .l10n.php files are omitted; WordPress can use the corresponding .mo files. Foreign translation names and executable translation payloads are rejected. Core language packs remain outside this registry contract and are handled by WordPress.

Failure and withdrawal

An unavailable or invalid registry is an error, not proof that extensions are current. The client shows an administrator warning and never silently falls back to WordPress.org for plugins and themes. Installed software continues running, and core updates continue through WordPress. Withdrawn packages lose installation authorization; promoting an older retained release changes future offers but does not force existing sites to downgrade. Archive withdrawal does not uninstall code already running on a site.

Failure indicators are bounded to ten operations for the configured provider and persist until a fresh authenticated response succeeds for the affected operation. A successful catalogue request cannot hide an unresolved plugin/theme update failure. Cached metadata and elapsed time do not clear these indicators.

Full catalogue readiness has two distinct measures: imported metadata and verified, locally available archives. A resumable importer is not evidence that the full upstream catalogue has already been copied. Monitor both counters, import errors, disk capacity and cron execution before opening the registry to clients. Public API serving never downloads missing packages on demand.

Development and evidence

Run from configs/:

npm run test:registry
npm run test:registry:publishers
npm run build:plugins
npm run package:registry
npm run package:registry:cpanel
npm run test:registry:cpanel

PHP_BINARY selects a PHP runtime. The registry runner loads Sodium/ZIP for the command when available but disabled, without editing php.ini. The complete deployment ZIP is dist/releases/wp-registry-cpanel.zip; it contains both public domain roots, the private application, the client template, a private configuration example, an installation guide and checksums. wp-registry-server.zip is a source archive for development rather than the complete cPanel delivery. Neither includes a populated catalogue or signing credentials.

Tests cover authenticated metadata, tampering, expiry, replay, cache bounds, outage behavior, native wire formats, exact update targets, archive integrity, namespace checks and one-time removal of old core offers. They prove native core requests bypass the registry while plugin/theme protection remains active. Server and administration tests cover publication, ownership, CSRF, uploads, release changes and safe rendering. Real cPanel TLS/authentication, filesystem policy, complete upstream import and production load require deployment/staging verification. Local harnesses and a successful build do not establish those production properties.

npm run test:registry:wordpress additionally requires the local WordPress core in dist/ and private UR_TEST_MYSQL_DSN, UR_TEST_MYSQL_USER, and UR_TEST_MYSQL_PASSWORD environment variables. The DSN must identify a local MySQL server without a database name. This test copies only native core files into a temporary directory, creates a unique temporary database, and intercepts all HTTP and mail. It exercises plugin and theme installation followed by a newer registry release, and verifies that a same-version core reinstall uses only native WordPress fixture endpoints. It never loads the project's configuration or activates this plugin on the existing site. Both isolated resources are removed afterward. Same-version core reinstallation does not prove cross-version database migrations.

Upstream contracts: native WordPress version checks, HTTP request interception, package download interception.