Goal
Make your sibling plugin refuse to wire up any of its hooks unless two conditions hold:
mam-mainis present and recent enough — the runningMAM_VERSIONis at least the version your plugin depends on.- The customer is entitled to your plugin — the
mam_plugin_entitlementfilter reports a non-blocked state for your plugin slug.
When either check fails, the constructor returns early and registers nothing. The plugin stays dormant instead of throwing fatals against APIs that aren’t there or running code the customer hasn’t paid for. The entitlement half of this gate is exactly what every shipped sibling uses (mam-geofilters, mam-reviews-manager, mam-contact-form); adding the MAM_VERSION check in front of it is the recommended extra step when your plugin depends on a specific mam-main release. Following the pattern keeps your plugin consistent with the rest of the Suite.
This is the single guard that decides whether your plugin runs. Put it at the very top of your bootstrap class constructor, before any
add_filter/add_actioncall. Everything after the guard can assumemam-mainis loaded, current enough, and licensed.
Prerequisites
- A sibling plugin that follows the Suite bootstrap convention: a main plugin file with a controller class hooked on
plugins_loaded, instantiated through a staticinit()callback. (See Recipe: Scaffold a new MAM sibling plugin if you don’t have this yet.) - A unique plugin slug that matches your plugin’s text domain and the slug the licensing server knows it by — e.g.
mam-geofilters. The entitlement system keys on this exact string, and the remote sync only tracks slugs that begin withmam-. mam-maininstalled and active in your dev environment so themam_plugin_entitlementfilter is registered.
Background: what each check actually reads
You don’t need to build any new infrastructure — mam-main already exposes both signals.
-
Version.
mam-maindefines aMAM_VERSIONconstant (currently26.29.15, CalVer, derived from its pluginVersion:header). Reading it withversion_compareis the cheapest, most direct version gate. If the constant is undefined,mam-maineither isn’t loaded or is too old to expose it — treat that as “fail the gate.” -
Entitlement.
mam-mainregisters amam_plugin_entitlementfilter that takes( $default, $plugin_slug )and returns a state array. The state is read from a locally cached option (mam_plugin_entitlements) that a background sync refreshes from the licensing server — the filter itself never makes an HTTP call, so it’s safe to run synchronously on every request. The returned array always has astatekey, one of:stateMeaning Should you bootstrap? entitledPurchased, or running on the source server, or a cold-cache fail-open Yes graceSubscription lapsed but inside the grace window Yes expiredSubscription expired past grace Yes, but do not register update info blockedExplicit “not purchased” from the server No — return early Note the deliberate fail-open behavior: on a cold cache or an unknown slug the filter returns
entitledso a licensing-server hiccup never dark-ships a feature the customer paid for. Only an explicitblockedsignal stops your plugin.
Steps
1. Confirm mam-main is loaded at all
The presence of the entitlement filter is your proof that mam-main booted. Bail immediately if it isn’t there:
public function __construct() {
if ( ! has_filter( 'mam_plugin_entitlement' ) ) {
return;
}
// ...
}
Because your controller is hooked on plugins_loaded, every Suite plugin’s main file has already run by this point, so a missing filter genuinely means mam-main is absent — not just late.
2. Gate on the mam-main version you require
Decide the minimum mam-main version your plugin depends on (for example, the release that introduced a hook or JSON key you consume), then compare against MAM_VERSION:
$required_main = '26.19.0'; // the mam-main release your plugin needs
if ( ! defined( 'MAM_VERSION' )
|| version_compare( MAM_VERSION, $required_main, '<' ) ) {
return;
}
Returning early here means an out-of-date mam-main simply leaves your plugin dormant rather than calling hooks that don’t exist yet. If you want the admin to understand why the feature is missing, register an admin_notice before returning — but keep the early return so no feature code runs.
Pick the lowest version that actually works. Over-tightening this number strands customers who are one release behind. If your plugin only relies on long-standing hooks, you may not need a version gate at all — but defining the dependency explicitly documents it.
3. Read the entitlement state for your slug
Store your slug on the class and run it through the filter:
private $plugin_slug = 'mam-your-plugin';
// inside __construct(), after the version gate:
if ( empty( $this->plugin_slug ) ) {
return;
}
$entitlement = apply_filters( 'mam_plugin_entitlement', null, $this->plugin_slug );
if ( empty( $entitlement ) || 'blocked' === $entitlement['state'] ) {
return;
}
Pass null as the default, exactly as the shipped siblings do — the entitlement manager always returns a populated state array, so the default you pass is never actually consulted. Only blocked (and an unexpectedly empty result) stops the bootstrap; entitled, grace, and expired all proceed.
4. Skip update registration when expired
Past this point your plugin runs, but an expired subscription should not keep pulling private plugin updates. Guard the update-info registration on the state:
if ( 'expired' !== $entitlement['state'] ) {
do_action( 'mam_set_plugin_update_info', basename( __DIR__ ), __FILE__ );
}
The mam_set_plugin_update_info action registers your plugin slug and file with mam-main‘s private update checker. Firing it for entitled and grace keeps the customer on the latest build; skipping it for expired is how a lapsed subscription stops receiving new versions while existing functionality keeps working.
5. Register your hooks below the guard
Everything after the guard is your normal wiring. Because the guard passed, you can call mam-main hooks without re-checking:
// ... guard above ...
add_filter( 'mam_your_plugin_is_active', '__return_true' );
add_filter( 'mam_get_phone_data_before_send', array( $this, 'inject_payload' ) );
// ...register the rest of your filters/actions here
}
Exposing an ..._is_active filter that returns true (as mam-geofilters does) is the idiomatic way to let other siblings detect that your plugin is installed, active, and entitled — they just check your filter instead of duplicating this whole gate.
6. Verify each branch
Confirm the gate behaves in all four situations:
mam-maindeactivated → your plugin registers nothing, no fatals. (has_filtercheck.)mam-maindowngraded below your required version → dormant. (MAM_VERSIONcheck.)- Slug
blocked→ temporarily force a blocked entry into themam_plugin_entitlementsoption and confirm the plugin stays dormant and the “MAM Plugin Access Required” admin notice lists your slug. - Slug
expired→ confirm the plugin’s features still load butmam_set_plugin_update_infodoes not fire (the plugin stops appearing in available updates).
Full guard, assembled
public function __construct() {
// 1. mam-main present?
if ( ! has_filter( 'mam_plugin_entitlement' ) ) {
return;
}
// 2. mam-main recent enough?
$required_main = '26.19.0';
if ( ! defined( 'MAM_VERSION' )
|| version_compare( MAM_VERSION, $required_main, '<' ) ) {
return;
}
// 3. licensed for this plugin?
if ( empty( $this->plugin_slug ) ) {
return;
}
$entitlement = apply_filters( 'mam_plugin_entitlement', null, $this->plugin_slug );
if ( empty( $entitlement ) || 'blocked' === $entitlement['state'] ) {
return;
}
// 4. update registration only when not expired
if ( 'expired' !== $entitlement['state'] ) {
do_action( 'mam_set_plugin_update_info', basename( __DIR__ ), __FILE__ );
}
// 5. ...register your hooks below this line...
}
Notes and gotchas
- Match the slug exactly. The licensing server, the
mam_plugin_entitlementscache, your text domain, and the string you pass to the filter must all be identical. A mismatch reads as a cache miss, which fails open — you’d ship the feature to unlicensed sites without noticing. - Don’t call HTTP yourself. The entitlement filter is intentionally synchronous and cache-backed. Never add your own license call in the bootstrap path; let the background sync own that.
- The guard runs once, at
plugins_loaded. It is not re-evaluated per request beyond that. Entitlement changes (a purchase, an expiry) take effect on the next page load after the background sync refreshes the cache. - Version gate is for compatibility, entitlement gate is for licensing. Keep them separate and ordered: presence → version → entitlement. The version check protects you from missing APIs; the entitlement check protects revenue.
Related
- Recipe: Scaffold a new MAM sibling plugin — the bootstrap skeleton this guard drops into.
- Reference:
mam_plugin_entitlementfilter — the full state-array contract and fail-open semantics. - Reference:
mam_set_plugin_update_infoaction — registering with the private update checker. - Explanation: How the MAM entitlement cache stays current — the background sync and
mam_plugin_entitlementsoption.
What’s next
Once the gate is in place, expose your own mam_<plugin>_is_active filter so sibling plugins can depend on yours the same way — see Recipe: Let other plugins gate behavior on yours.
