Gate your sibling plugin behind version + entitlement checks

Goal

Make your sibling plugin refuse to wire up any of its hooks unless two conditions hold:

  1. mam-main is present and recent enough — the running MAM_VERSION is at least the version your plugin depends on.
  2. The customer is entitled to your plugin — the mam_plugin_entitlement filter 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_action call. Everything after the guard can assume mam-main is 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 static init() 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 with mam-.
  • mam-main installed and active in your dev environment so the mam_plugin_entitlement filter 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-main defines a MAM_VERSION constant (currently 26.29.15, CalVer, derived from its plugin Version: header). Reading it with version_compare is the cheapest, most direct version gate. If the constant is undefined, mam-main either isn’t loaded or is too old to expose it — treat that as “fail the gate.”

  • Entitlement. mam-main registers a mam_plugin_entitlement filter 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 a state key, one of:

    state Meaning Should you bootstrap?
    entitled Purchased, or running on the source server, or a cold-cache fail-open Yes
    grace Subscription lapsed but inside the grace window Yes
    expired Subscription expired past grace Yes, but do not register update info
    blocked Explicit “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 entitled so a licensing-server hiccup never dark-ships a feature the customer paid for. Only an explicit blocked signal 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-main deactivated → your plugin registers nothing, no fatals. (has_filter check.)
  • mam-main downgraded below your required version → dormant. (MAM_VERSION check.)
  • Slug blocked → temporarily force a blocked entry into the mam_plugin_entitlements option 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 but mam_set_plugin_update_info does 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_entitlements cache, 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.

  • Recipe: Scaffold a new MAM sibling plugin — the bootstrap skeleton this guard drops into.
  • Reference: mam_plugin_entitlement filter — the full state-array contract and fail-open semantics.
  • Reference: mam_set_plugin_update_info action — registering with the private update checker.
  • Explanation: How the MAM entitlement cache stays current — the background sync and mam_plugin_entitlements option.

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.

Was this article helpful?
Contents

    Need Support?

    Can't find the answer you're looking for? Don't worry we're here to help!