M MemberIntel KB
Activity Decisions

decision

ADR-0043: One connected site is one subsite; network activation is allowed

ADR-0043 (Proposed — `ai-engineer` review complete (2026-07-30), `ceo-blair` sign-off pending on the cost condition, Seth's answer pending on pricing (see *Open questions*), 2026-08-01): One connected site is one subsite; network activation is allowed.

Status: Proposed — ai-engineer review complete (2026-07-30), ceo-blair sign-off pending on the cost condition, Seth’s answer pending on pricing (see Open questions)
Date: 2026-08-01
Deciders: Seth (decision owner). ai-engineer review: docs/superpowers/reviews/2026-07-30-ai-engineer-adr-0039-multisite.md — filed under the ADR’s original assumed number, see Numbering below. Raised by Omar at the 2026-07-28 Innovations meeting.
Related: #775 (the decision issue), #737/#894 (multisite scoping, merged), #897/#933 (legacy-DB MemberMouse, merged), #887, #889, #898 (per-subsite provisioning), #942 (identity, item 3), #750 (ingest cost, closed), ADR-0028 (profile-aware retrieval), ADR-0041 (Free bounded by member count), ADR-0036.

Numbering. #775 assumed ADR-0039 and flagged that the number needed re-checking before filing. It collided — 0039-achievements-are-append-only.md exists and the sequence runs to 0042. This is ADR-0043. The ai-engineer review keeps its original adr-0039 filename because it is already merged and linked from #775 and #898; renaming it would break those references.

This ADR differs from #775’s proposed decision on three of five items. Items 4 and 5 are amended per the review, and item 3 is demoted from a gate to a tracked issue. Where this text and #775’s body disagree, this text is the decision.

Context

MemberIntel had no multisite policy. The behaviour it shipped was accidental rather than chosen, which is the actual problem this ADR solves — not “should we support multisite” (Seth confirmed at the 2026-07-28 meeting that we must, given how many customers run it), but “what did we already commit to by accident, and is it what we want.”

Verified against wordpress/plugins/memberintel-connect on origin/main:

  • All connection state lives in per-blog options — memberintel_api_key, memberintel_site_url, memberintel_install_uuid, memberintel_connected (includes/class-api-key.php).
  • There is no Network: header in the bootstrap. That does not prevent network activation; it means nothing handled it.
  • No switch_to_blog() call exists anywhere in the plugin.

Because options are per-blog, network activation produces N independent unconnected installs, each minting its own key and UUID and occupying its own billed slot. So “one site = one subsite” is already the behaviour. It was never decided, documented, or defended.

Two defects sat inside the question, both now fixed:

  • Engagement data crossed subsite boundaries (#737). resolve_user_ids() resolved member emails against $wpdb->users, which is network-global, with no blog_id scoping — so one person with accounts on two subsites got subsite B’s activity merged onto their subsite A record. Fixed in #894 by inner-joining the blog-prefixed capabilities row, the same way WP_User_Query scopes by blog_id, centralised in the Scopes_To_Site trait so all six engagement modules were fixed at once. The same audit criterion caught #887 (BuddyPress) and #889 (LearnDash).
  • Legacy-DB MemberMouse read a network-wide table (#897). MM_TABLE_* is table-scoped on modern installs where MM_PREFIX = $wpdb->prefix, but legacy-DB mode uses an unprefixed mm_user_data. Fixed in #933 by refusing the module on that configuration — a cross-tenant leak on the billing path, where the product’s claims can least afford to be wrong.

Decision

One connected site is one subsite (blog_id). Never a network. Network activation is allowed, with per-subsite lazy provisioning.

1. Site = subsite. Make the existing behaviour the stated policy. Each subsite is its own MemberIntel tenant, with its own Site row, key, UUID and billed slot.

blog_id is safe even under a multi-network install: wp_blogs is one table for the whole installation keyed by blog_id, with site_id as a separate column naming which network a blog belongs to. blog_id does not reset or collide per network. “Multisite” in this ADR’s title covers multi-network configurations too.

This is also what lets ADR-0028’s profile-aware retrieval work here. Per-subsite Site rows keep archetype, business model and currency distinct; aggregating a network into one profile would collapse them into a single cohort match and break the ADR-0028 boost. That argument previously existed only in a comment thread on #775.

2. Multisite support requires the scoping fixes — satisfied. #737 (via #894) and #897 (via #933) are both merged. Narrowed from #775’s original wording: legacy-DB MemberMouse installations require the same scope audit, not “any multisite install.”

3. Settle on one identity value — tracked, not gating. The canonical value should be per-subsite home_url(), matching Dashboard_Controller, Chat_Controller, Plugin_Updater and Connection_Tab. It needs a migration plan, because the backend resolves sites by the URL the plugin sends and a naive rename can orphan existing connections. Tracked as #942.

This is explicitly not a gate on item 4. The subdirectory identity bug already exists on any single connected multisite subsite, network-activated or not; blocking network activation on it would ask item 4 to solve a problem it did not create. #898’s 03f35d47 stopped ensure() seeding memberintel_site_url, so the migration population is now bounded to sites that connected before that commit rather than growing with every subsite anyone opens.

4. Allow network activation, via lazy per-subsite provisioning. This amends #775’s item 4, which proposed refusing network activation with an admin notice until items 1–3 landed.

API_Key::ensure() provisions each subsite’s api_key/install_uuid the first time an admin with manage_options loads that subsite’s admin, and again on the connect path, with no cross-site writes and no backend call. Provisioning a key is not connecting a site. ensure() leaves memberintel_connected false; only mark_connected() flips it, from a deliberate human connect flow.

The refusal policy was answering an unbounded-Free-slot fear that is not true of this mechanism. What network activation does expose is whatever data-leak bugs are unfixed at the time — which is why this is gated on the item 2 fixes, both now merged, and not on item 3.

No switch_to_blog() is needed. admin_init, the REST handlers and every module query already run inside WordPress’s own per-blog request context, because multisite’s URL routing switches the DB table context before plugin code runs. Network activation itself does no cross-site work. A future feature that needed cross-subsite awareness in one request — a “list every subsite in this network” screen — would need get_sites() plus switch_to_blog()/restore_current_blog(); nothing here builds that.

5. Do not gate the module set on Free for multisite. This rejects #775’s item 5 outright. It was conditioned on #750 returning a measured ingest cost; #750 closed on 2026-07-29 and the number clears with an order of magnitude to spare — Free at $0.114–$0.394/tenant/month across the 100–2,500-tenant band ($0.132 at 1,000) against a ≤$1.10 target, Pro at $0.244/tenant/month at 1,000 tenants against $6–12.

Cost was never the binding constraint. Capacity is, and it is tier-independent: a full-module-set sync breaks the sweep’s per-site time budget above roughly 308–360 members per platform, and that hits Pro at least as hard as Free — arguably worse, since Pro syncs daily. Gating Free specifically would aim a fix at the wrong axis. This is consistent with ADR-0041, which bounds Free by member count rather than by connector platform.

Consequences

Positive:

  • The shipped behaviour is now the chosen behaviour, with a written rationale instead of an accident.
  • Network activation works rather than half-working: previously it seeded whichever subsite was current and left the rest keyless and unable to connect at all (#888).
  • Per-subsite tenancy preserves ADR-0028 retrieval quality; no archetype mixing.
  • The cross-subsite engagement leak and the legacy-DB MemberMouse leak are both closed before network activation is blessed.

Negative / costs:

  • No per-network billing bundle exists. A 20-subsite operator who connects all 20 subsites incurs 20 independent addon_billable charges, priced identically to 20 standalone sites. Making network activation easy will exercise this gap sooner than otherwise. “Network activation allowed” does not mean pricing is resolved — see open question 1.
  • Item 3 remains live: on a subdirectory install the connect paths still disagree about site identity (#942).
  • The capacity ceiling above ~308–360 members per platform is unaddressed here and applies to every tier.
  • Provisioning is per-subsite and lazy, so a subsite whose admin nobody ever opens holds no key until someone completes a connect — correct, but it means “network activated” and “provisioned” are not the same state.

Mitigations:

  • #942 owns the identity migration, with the surface already reduced by 03f35d47.
  • The manage_options gate on maybe_ensure() keeps admin_init from letting an unauthenticated admin-ajax.php/admin-post.php request mint a subsite’s key — admin_init is not admin-only.
  • The two-subsite verification rig that #894 and #898 both relied on should become a runbook or a WP-multisite integration harness. The Brain Monkey unit suite has no WordPress and no database, so it can pin query shape but cannot demonstrate that a foreign subsite’s user is excluded. That check currently lives only in PR descriptions, and the next connector-scoping audit will need it again.

Alternatives considered

  • Network-level connection, one slot, aggregated data — rejected. Collapsing unrelated subsites into one profile mixes archetypes, business models and currencies, degrading advisor output and defeating both the ADR-0028 retrieval boost and cohort benchmark matching. Revisit if a customer asks for a portfolio view, and build it as a view over N connections rather than one merged site.
  • Network activation with per-subsite opt-in in Network Admin — the right long-term shape, but it needs the same scoping fixes first and there is no demand signal yet.
  • Refuse network activation until items 1–3 land (#775’s original item 4) — rejected as worded. It answers a Free-slot fear that this provisioning mechanism does not create, and gating on item 3 asks it to fix a bug that already exists on manual per-subsite activation.
  • Ship as-is, decide later — rejected. #737 was a live leak on shipped modules, and the accidental policy was never defended.

Open questions

These need Seth and Blair, not engineering:

  1. Is per-subsite billing commercially acceptable, or does a network operator expect one price? If per-network pricing is wanted, the recommendation is a billing bundle over N slots rather than aggregating data. Nothing implements this today.
  2. Does Free need a per-network slot cap, separate from the per-site operation caps?
  3. Multi-network installs — same policy, or explicitly unsupported at launch? Recommendation: explicitly unsupported until someone asks.

Acceptance

  • ai-engineer review — 2026-07-30, merged via #899
  • #737 fixed (#894) and #897 fixed (#933)
  • Identity issue filed with an owner (#942)
  • Seth answers open question 1 (pricing)
  • ceo-blair sign-off on the cost condition
  • This ADR moves to Accepted
For: S Seth Shoultes A AI Engineer B Blair Williams S Santiago Perez Asis P Product Lead