decision
ADR-0036: Connector shared record format for multi-platform modules
ADR-0036 (Accepted, 2026-07-17): Connector shared record format for multi-platform modules.
Status: Accepted
Date: 2026-07-17
Deciders: ai-engineer (technical sign-off), product-lead (product sign-off)
Review log
- 2026-07-17 —
ai-engineer: APPROVE-WITH-CHANGES. Core design (namespaced IDs,platform
tag,kindaxis, additive wire compat) sound and proceeds to this ADR. Required before
acceptance: resolve the EDDplatform_idambiguity, commit to explicitschema_versionin
the envelope, flip thecurrencynull-fallback default. All three applied to the spec.
WishList failure-mode isolation, per-page fan-out budget, eval-fixture scoping, and the
content_rules_summaryretrieval-projection contract recorded as explicit backend
follow-up-spec requirements (spec §12), not blocking this ADR.
Full write-up:docs/superpowers/reviews/2026-07-17-ai-engineer-connector-shared-record-format.md. - 2026-07-17 —
product-lead: GO-WITH-CHANGES. Wire-format contract sound. Required before
acceptance: name the multi-platform dashboard experience as an owned follow-up (not
“invisible plumbing”), cut the BuddyBosscourse_idsmembership-enrichment hitchhiking from
this pass, keep BP-only users dropped in V1 with the conversion-signal idea parked as a
separate future PRD needing Blair sign-off, keep cross-platform email matching strict with an
explicit fail-closed statement. All four applied to the spec.
Full write-up:docs/superpowers/reviews/2026-07-17-product-lead-connector-shared-record-format.md.
Context
memberintel-connect (the WordPress plugin at wordpress/plugins/memberintel-connect/) reads
MemberPress only today — its Data_Controller hardcodes MeprProduct/MeprUser/
MeprTransaction/MeprSubscription checks and raw SQL against wp_mepr_* tables. Every route
(/memberships, /members, /transactions, /subscriptions, /content, /stats) returns one
platform’s shape.
innovations#303 asks us to generalize the
plugin into a multi-platform connector with per-platform modules — MemberMouse (#301), WishList
Member (#302), Easy Digital Downloads (#299/#300), and a BuddyPress/BuddyBoss engagement module
(#289/#320) — that can co-exist on one site. #303’s own open question was the blocker: confirm
the shared record format (what a “member,” “membership,” and “transaction” look like across every
platform) before building the second module, so the first module doesn’t need reworking.
The full design rationale, field-by-field schema, and per-platform status-mapping tables live in
the spec: docs/superpowers/specs/2026-07-17-connector-shared-record-format-design.md. This ADR
records the material choices from that spec that need durable, discoverable sign-off — it does
not restate the spec in full.
Decision
Adopt the shared record format as specified, with these material choices:
-
Namespaced string IDs, not raw integers. Every entity’s
idbecomes"{platform}:{platform_id}"(e.g."memberpress:42"), so a MemberPress user #42 and an EDD customer #42 on the same site don’t collide on a single global ID. This is the load-bearing schema change — the backend site-sync pipeline must key on namespaced IDs going forward. -
Every record carries a required
platformtag, enumerated tomemberpress | memmouse | wishlist | edd | buddypress. Retrieval, playbooks, and the backend all key off this field — it’s the mechanism that keeps (for example) a MemberMouse site from getting MemberPress-specific advice. -
A
kindaxis distinguishesbillingplatforms (emitmember/membership/transaction/subscription— MP, MemberMouse, WishList, EDD) fromengagementplatforms (emit no billing entities; enrich billing-platform member records via an email join — BuddyPress/BuddyBoss). A site can carry multiple billing platforms simultaneously (#300’s MP+EDD case) and up to one engagement platform in V1. -
Cross-platform join key: email, lowercased and trimmed, fail-closed. No fuzzy matching. A non-match means the engagement-side record contributes nothing — never a fallback into a possibly-wrong merge. When the same email legitimately belongs to member records on more than one billing platform, the engagement object is broadcast to all of them (not exclusively assigned to one).
-
BuddyBoss is not a separate platform value. Per #289’s field-testing, BuddyBoss Platform forks BuddyPress and preserves its core REST namespace; one
platform: "buddypress"module opportunistically reads BuddyBoss-Pro fields when present. -
Wire-format versioning: explicit
schema_version: intin the response envelope, independent of the plugin’sMIC_VERSIONsemver. Pre-this-ADR responses have noschema_versionfield at all — that absence is treated as an implicit v1; this spec’s shape adds an explicitschema_version: 2. The backend accepts both the implicit-v1 (absent) and explicit-v2 shapes until the 2026-Q4 minimum-version ratchet. Plugin release version and wire schema version are different axes — coupling them would force the backend’s version-detection logic to reason about semver ranges instead of a simple integer comparison. -
currencyis emitted at ingest whenever available, preferring per-record currency over a platform’s site-level default (MEPR_CURRENCY_CODE,edd_get_currency()are both the store’s single configured setting, not per-order data — use them only when no per-record currency exists).null → site-profile fallbackis the exception path for records with neither, not the default — multi-currency EDD add-ons are exactly the case where a site-level value would be an unreliable stand-in for the actual order currency. -
EDD’s compound ID (
edd:{product_id}:{price_id}, needed for Recurring’s per-tier billing) keepsplatform_idas the bareproduct_id, with a newprice_idfield carrying the tier separately — avoiding a parsing landmine where a genericid.split(':', 1)(correct for every other platform) would silently mis-parse the embedded colon. -
BP-only users (community members with no paid membership) stay out of scope in V1.
/membersonly surfaces members with at least one billing-platform record. Admitting BP-only rows is a product decision with data-subject-classification and retrieval-cardinality implications — deferred to a future PRD requiring its ownproduct-lead+ceo-blairsign-off (and acounselpass, per the same data-handling bar as the global-brain de-identification work), not decided as a side effect of this schema. -
content_rules_summarystays inlined onmembership(no dedicated/content/rulesendpoint), with a retrieval-layer requirement that the formatting layer summarize gated-content counts rather than serialize raw ID arrays into a prompt. The previously-proposed BuddyBosscourse_idsenrichment of this same field is cut from this pass as unscoped, single-partner speculation — not part of this decision.
Consequences
Positive:
- The four new connector modules (MemberMouse, WishList, EDD, BuddyPress) can be built against one settled contract without reworking the MemberPress module or each other.
- Namespaced IDs + the
platformtag give retrieval a server-side mechanism to avoid cross-platform advice bleed, rather than relying on prompt convention. - Explicit
schema_versionavoids coupling wire-schema evolution to plugin release cadence. - Existing MemberPress installs are unaffected — every change is additive; sites on the old plugin version (no
schema_versionfield at all — v1 is implicit from its absence) see a byte-identical response to today.
Negative / costs:
- A site running all 5 modules means up to 5 separate per-page reads (4 SQL scans + 1 external REST call) versus today’s single read — real ingest-side compute cost, not yet budgeted against shared hosting (tracked as a required backend-follow-up item, spec §12).
content_rules_summaryand the engagement email-join both introduce new GDPR/CCPA deletion-pathway surface area that the current deletion flow doesn’t yet handle (tracked, spec §12).- Three new per-platform status-mapping tables (spec §9) have zero eval-fixture coverage today; a wrong mapping silently corrupts MRR/active-member figures (tracked, spec §12).
Mitigations:
- All cost/robustness gaps above are named as explicit, non-optional requirements in the backend-side follow-up spec (spec §12) rather than left implicit — they must be addressed before the first new module (#301 or #302) ships to a real customer, per the ship-gate requirement product-lead added to that same follow-up spec.
- The multi-platform dashboard/support experience is scoped as its own short UX spec before the module-registry refactor spec locks the API shape further, so it isn’t discovered mid-implementation.
Alternatives considered
- A single global integer ID space — rejected; can’t guarantee uniqueness across platforms without a central ID-issuing authority the plugin doesn’t have, and would require rewriting the existing MemberPress wire contract instead of extending it.
- Union of every platform’s native status enum, surfaced as-is — rejected; bleeds platform detail into the canonical layer and forces retrieval/playbooks to know 4 status vocabularies instead of 1.
- Synthesizing
engagementcounters for platforms that don’t natively track them (e.g. WP-login-count-equivalents fromusermetafor MP/WishList/EDD) — rejected; expensive per-row compute with no clean semantics and no natural stopping point for whichusermetakeys count as “engagement.” - EDD price tiers as a
variants: []sub-field on one membership row per Product — rejected; retrieval and playbook targeting need each price tier independently addressable as its own record. MIC_VERSION(plugin semver) as the schema-version signal — rejected; couples two independent axes (release cadence vs. wire-schema shape), forcing semver-range reasoning into what should be a simple integer comparison.- Admitting BP-only users into
/membersnow — rejected for V1 (see Decision #9); real advisor value in the idea, but it changes MemberIntel’s data-subject scope and needs its own product/consent/counsel review, not a schema-level default.