Harbor introduces unified licensing with LWSW--prefixed keys. This changes the relationship between Harbor instances: one instance becomes the fat leader (owns all unified licensing concerns) and every other instance becomes a thin instance (declares itself to the leader, then gets out of the way).
In per-resource licensing (uplink v2/v3), every instance was self-sufficient. It stored its own key, validated against the licensing APIs, rendered admin fields, and managed its own state. The leader was just a tiebreaker for shared UI. Unified licensing inverts this: the leader takes over key storage, API delegation, feature catalogs, and the admin page. Individual instances become thin shells.
For the unified key model itself: how the key relates to Licensing, Portal, and the WordPress site, how seats work, and what happens in various key change scenarios. See Unified License Key: System Design.
Multiple vendor-prefixed copies negotiate leadership through a shared global function, _lw_harbor_instance_registry(), defined in src/Harbor/global-functions.php. Because global functions are declared once (PHP's function_exists guard), the static variable inside that function is shared by all vendor-prefixed copies regardless of which one's file was included first. Each instance calls _lw_harbor_instance_registry( Harbor::VERSION ) during bootstrap to register itself. Registrations are only accepted before wp_loaded, so all real instances (which initialize on plugins_loaded) can register, but nothing can inject fake versions after the bootstrap window closes.
Version::is_highest() (in src/Harbor/Utils/Version.php) reads the registry and returns true if this instance's version string is greater than or equal to all registered versions. Version::should_handle( $action ) layers a per-responsibility mutex on top: it fires do_action( 'lw-harbor/handled/{action}' ) the first time a qualifying instance claims a responsibility, and any subsequent call (even from the same instance) sees did_action() return true and backs off. This ensures exactly one instance handles each shared responsibility — admin page, REST routes, etc. — even when two copies run the same version.
The leader stores the site's unified key and the full product catalog response from the v4 licensing API. The key is the site's identity to Licensing; the catalog response is the source of truth for what products are entitled, what tiers they're on, and whether seats are available. See Key Management in the system design doc for how keys enter a site and the one-key-per-site rule.
The leader delegates to the v4 licensing client via LicensingClientInterface from stellarwp/licensing-api-client. products()->catalog($key, $domain) is a read-only bulk fetch that returns the status of all products under the key without consuming seats.
The production implementation uses WordPressApiFactory from stellarwp/licensing-api-client-wordpress, which routes requests through WordPress's HTTP API. License_Repository wraps the client with option-based state storage so the rest of Harbor never touches the client directly. Fixture_Client (src/Harbor/Licensing/Clients/Fixture_Client.php) implements LicensingClientInterface for testing by reading from JSON fixture files.
The leader fetches a feature catalog from the Commerce Portal API using the unified key. Products query features through global functions defined in src/Harbor/functions.php — is_feature_enabled() and is_feature_available(). Today the catalog fetch has no key to authenticate with; the unified key is what unblocks this.
The leader renders the unified licensing page (the "Software Manager"). It shows the unified key status, product registrations, and legacy key cards that link back to each product's own licensing page.
A thin instance is any Harbor copy operating under a unified LWSW- key. It still creates its resource and license objects — it needs to know what product it is — but when it detects a LWSW- key, it skips wiring into v2/v3: no per-resource validation hooks, no per-resource admin fields, no per-resource API calls. The v4 path short-circuits the v3 machinery.
What a thin instance does: if it ships with an embedded key, it bundles an LWSW_KEY.php file that the leader discovers automatically on key-discovery. On plugin activation or key entry, it fires an action that the leader handles. And it queries the leader through global functions like is_feature_enabled().
What a thin instance does not do: validate keys, talk to licensing APIs, or render licensing admin fields.
All communication between vendor-prefixed copies happens through non-prefixed WordPress hooks. This is the only mechanism available — each copy has its own namespace and cannot reference another copy's classes.
Products declare themselves to the leader through a cross-instance filter. Each instance contributes its slug, display name, product (kadence, give, the-events-calendar, learndash), and a contributed key if it has one. The leader reads this filter lazily, by the time it's consumed, all plugins have loaded and registered.
A product does not provide its tier. Tiers come from the v4 licensing API response, they're a property of the license, not the product.
Products using per-resource v2/v3 keys register metadata so the leader can display informational cards in the unified admin UI. The leader does not validate legacy keys. It displays them as-is with a link to the product's own licensing page. Per-resource keys continue through the existing per-resource path unchanged.
The leader delegates license operations back to the owning instance through shared hooks. Harbor::register_cross_instance_hooks() in src/Harbor/Harbor.php wires this up: each instance listens for lw-harbor/validate_license, lw-harbor/set_license_key, and lw-harbor/delete_license_key, but only acts on resources present in its own collection.
- Replace per-resource licensing products that haven't adopted it continue to use v2/v3 as-is
- Validate legacy keys the leader only displays them; validation stays in the per-resource path
- Build the v4 licensing client that's an external package; Harbor delegates to it
- Migrate existing keys no automatic conversion from per-resource to unified
- Assign tiers tiers come from the v4 API response, not from product declarations