How sibling plugins extend your app: the filter-driven architecture

The core idea

MAM Suite is often described as “the WooCommerce of mobile apps.” That phrase is not just marketing — it describes a specific architectural commitment. Like WooCommerce, MAM ships a small core (mam-main) and a portfolio of feature plugins that each add one capability: a contact form, a chat tab, special offers, in-app purchases, geofiltered content, and so on. And like WooCommerce, those feature plugins do not reach into each other’s code. They inject their behavior into the core’s pipelines through WordPress filters and actions, and the core never imports them back.

This is the single most important thing to understand before you write or extend a MAM plugin:

mam-main never imports sibling-plugin classes. Sibling plugins hook into mam-main‘s pipelines. Extension flows in one direction — inward, through hooks.

The mam-main architecture reference states this as a hard rule: “Sibling plugins extend mam-main exclusively through filters and actions. mam-main never imports their classes; they hook into its pipelines.” Every design rule in the suite follows from that sentence.

Why it matters

A MAM customer site is not a fixed bundle. Each enrolled site runs mam-main plus whatever subset of feature plugins that customer is entitled to. One site has chat and special offers; the next has in-app purchases and geofilters; a third has a fully custom use-case plugin built just for them. The core cannot know in advance which plugins are present, so it cannot call them by name.

The hook-driven model solves this cleanly. A plugin that is not installed simply never registers its hook, so nothing breaks and the core needs no class_exists() special-casing. New features arrive as new subscribers rather than new core code, so mam-main stays small and stable. Each plugin is fully self-contained — it communicates only through WordPress hooks and does not include files from another plugin. And because extension is additive and opt-in, the same small core powers thousands of differently-configured, à-la-carte apps: exactly the WooCommerce model — a stable commerce core, an open ecosystem of extensions hanging off its hooks.

(For the full case — why imports would couple every sibling to mam-main‘s internals, how the rule fails gracefully on version-mismatched sites, and the trade-offs it accepts — see Why MAM Suite extends through filters and actions only (the no-import rule). This article is the companion piece: not why the rule holds, but how a sibling actually hooks in.)

The cost of this freedom is that order matters, and order is not obvious when plugins never reference each other. That is what filter priorities are for, and we will get to them below.

How an injection actually works

The mobile apps (iOS and Android) talk to the WordPress site through a single AJAX endpoint, local_app_get_phone_data. That endpoint returns one JSON payload describing every screen, button, tab, and content element the app will render. Building that payload is the job of the phone-data pipeline inside mam-main.

The pipeline runs in phases. Near the end of the content-building phase, it fires a filter — mam_get_phone_data_before_send — and passes the in-progress payload to every subscriber. This is “the main extension point for sibling plugins,” with roughly 70 subscribers across the suite. Each feature plugin registers a callback here, adds its own keys to the payload, and returns it. The next subscriber receives the enriched payload, adds its part, and so on, until the pipeline finalizes and ships the result to the app.

In practice the pattern is strikingly uniform. Open almost any feature plugin and you will find a single line that wires its phone-data handler into the pipeline. For example, mam-special-offers registers:

// mam_get_phone_data_before_send — injects special offers data into the mobile JSON payload.
add_filter( 'mam_get_phone_data_before_send', array( $phone, 'manage_phone_data' ) );

mam-chat-manager, mam-geofilters, and mam-inapp-purchase-manager each do the same thing with their own manage_phone_data method. None of them knows the others exist. They all know only the hook name and the shape of the payload they are handed. That shared, frozen payload shape — not shared code — is the contract between plugins (which is exactly why those hook names and payload keys are never renamed in place; see Frozen public contracts and the wrap-don’t-rename discipline).

This is the whole architecture in miniature: a plugin says “when you build the app payload, also call me,” adds its slice, and gets out of the way.

The hooks a sibling plugin lives on

The phone-data filter is the most visible extension point, but a feature plugin typically subscribes to several core hooks, each for a different job. The main surfaces are:

Hook What a sibling plugin does with it Type
mam_get_phone_data_before_send Inject its content into the mobile JSON payload. The primary surface — used by nearly every no-code feature plugin. filter
mam_tab_manager Enrich a button or tab definition (for example, attach the plugin’s content to a tab the user placed). filter
mam_notification_list Register a notification type with its own per-channel templates so the core dispatcher can send it. filter
mam_notification_send_message Fire a notification (email / SMS / push) through the core dispatcher. action
mam_app_settings_get_setting Read a setting through the core’s resolution chain instead of touching options directly. filter
mam_cron_manager Register a custom recurring task with the core cron processor. filter

You can see the layering in a real plugin. mam-special-offers registers a notification type (mam_notification_list), enriches the tab that hosts it (mam_tab_manager), and injects its data into the payload (mam_get_phone_data_before_send) — three different core pipelines, three hooks, zero imports. That is the texture of a typical MAM feature plugin.

A key consequence: a sibling plugin should reach the rest of the suite only through these hooks. If you find yourself wanting to include a file from another plugin or call its class directly, that is the signal you are fighting the architecture. The right move is almost always “find (or add) the filter and subscribe to it.”

Priority: how the order is decided

Because subscribers run in a chain and each one sees the payload built so far, the order they run in is part of the behavior. WordPress orders hook callbacks by their priority argument — the third argument to add_filter(), lower numbers first, default 10. MAM leans on this deliberately, and the phone-data pipeline has an established set of priority conventions.

For mam_get_phone_data_before_send, the convention is roughly:

Priority Who runs there Why
1 Base app settings (mam_app_settings) Establishes the foundational payload everything else builds on. Must run first.
10 The default cohort — most sibling plugins The normal place to inject feature content. This is the default, so most plugins do not pass a priority at all.
99–100 Late-running enrichments (faceted search, localization, payment gateways) Need the base content already present before they decorate it.
199 Form-state validation Runs after content but before the final shaping.
999+ Niche overrides (weather, delivery, specialized processors) Deliberately late so they win over earlier writers.
1000 Core home_cats injection (MAM_Main_Manager::manage_phone_data) Must run after all priority-<1000 subscribers, because it depends on content they have already added.
1001+ Use-case-specific total overrides (bespoke per-customer plugins) The last word — they can reshape the whole payload for a single customer’s app.

This is why most feature plugins you read — chat, geofilters, special offers — register with no explicit priority: they belong to the default 10 cohort and are happy to run in the middle of the pack. mam-inapp-purchase-manager passes 10 explicitly, which is the same slot stated for clarity. You only reach for a higher number when your plugin must see the work of others first, or must have the final say.

The mental model is a pipeline with reserved lanes: settings at the front, the general feature crowd in the middle, decorators behind them, and total overrides at the very back — with the core’s own home_cats step pinned at 1000 as the boundary between “normal feature work” and “full-payload override.”

How this connects to the rest of the Codex

The filter-driven model is the backbone that several other concepts hang from:

  • The phone-data pipeline is the concrete machine that fires mam_get_phone_data_before_send. If you want to know when in the request your callback runs and what the payload looks like at that moment, that is the pipeline’s story.
  • The single-payload data model explains why injecting into one shared JSON object — rather than each plugin owning its own endpoint — is the design that makes hook-based extension work at all.
  • The settings cascade is itself exposed as a hook (mam_app_settings_get_setting), so reading configuration follows the same “go through the core, not around it” discipline as everything else.
  • The caching model (cursor) matters the moment you write a subscriber: because subscribers share one cached payload, a callback that ignores the cache rules can serve stale or cross-user data. Hooking in is easy; hooking in correctly means respecting the cache contract.

If you remember one rule from this article, make it the one the core’s own architecture doc ends on: when your feature crosses a boundary between plugins, the answer is almost always “add a new filter and let subscribers register against it.” That is not a workaround in MAM — it is the architecture, and it is the reason the same small core can power thousands of different apps.


Verification

This article was last verified against:

  • mam-main/ARCHITECTURE.md — §1 (one-directional extension rule), §4.1 (phone-data pipeline), §5.3 (frozen hooks), §7 (Sibling Plugin Extension Points + priority conventions)
  • mam-suite/docs/ARCHITECTURE.md — suite-wide design rules
  • mam-special-offers/mam-special-offers.phpmam_notification_list, mam_tab_manager, mam_get_phone_data_before_send registrations
  • mam-chat-manager/mam-chat-manager.php, mam-geofilters/mam-geofilters.php, mam-inapp-purchase-manager/mam-inapp-purchase-manager.phpmanage_phone_data subscriber pattern and priority arguments

Re-verify whenever the phone-data priority conventions change, a primary extension hook is renamed or added, or the one-directional “core never imports siblings” rule is altered.

Was this article helpful?
Contents

    Need Support?

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