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 noblog_idscoping — 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 wayWP_User_Queryscopes byblog_id, centralised in theScopes_To_Sitetrait 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 whereMM_PREFIX = $wpdb->prefix, but legacy-DB mode uses an unprefixedmm_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_billablecharges, 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_optionsgate onmaybe_ensure()keepsadmin_initfrom letting an unauthenticatedadmin-ajax.php/admin-post.phprequest mint a subsite’s key —admin_initis 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:
- 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.
- Does Free need a per-network slot cap, separate from the per-site operation caps?
- Multi-network installs — same policy, or explicitly unsupported at launch? Recommendation: explicitly unsupported until someone asks.
Acceptance
-
ai-engineerreview — 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-blairsign-off on the cost condition - This ADR moves to Accepted