Skip to main content

ADR: Partner Module vs Partner Portal — ownership and relationship

Status: Accepted · Date: 2026-07-12 · Author: engineering audit This ADR settles how the two “partner” systems relate, who owns which data, and what the safe path is if the older system is retired. It exists because both systems use the word “partner” and are easy to conflate — but they are different products for different audiences on different identity keyspaces, and merging them would be a data-integrity mistake.

Context

Two systems carry the word “partner”: Findings that inform this decision (from the 2026-07-12 audit):
  • The Portal’s paid subscription was canceled for lack of use (FUND-1847, May 2026). The code still ships at HEAD, and a 15-minute Dub analytics-sync cron still runs, but the product is effectively sunset.
  • The Portal has a P0 tenancy hole: partnerOrganizations.create binds an org to a caller-supplied CRM locationId with no ownership verification — any authenticated user can read another location’s commission ledger and customer PII. It also has zero test coverage across its Convex functions.
  • The Module’s authorization boundary is solid at every layer (session-actor location scoping, admin-only cross-location, fail-closed on missing location), and it is well tested. Its launch risks are observability and data-shape semantics. The data-shape questions were live-verified on 2026-07-12 (amounts are whole USD dollars); the referred-contact mapping fix is in PR #1198 and must land before relying on Module lead linking.

Decision

  1. The Partner Portal is legacy / paused, not active. Treat it as maintenance-only. Do not build new Portal features. The one exception is the P0 tenancy fix, which must be resolved or the surface disabled regardless of sunset status because the code is still deployed (see “Consequences”).
  2. The Partner Module is the go-forward CRM-native affiliate system. It is not a drop-in replacement for the Portal — it serves a different audience (partners surfaced inside the CRM to operators/admins) on a different identity keyspace. It complements, and eventually supersedes, the Portal’s internal-facing affiliate-reporting use case; it does not replace the Portal’s external white-label partner login experience (no Module equivalent exists, and none is planned).
  3. Ownership of each concept is single-system. Do not cross the streams.
  4. A shared read-only attribution adapter is permissible but not required now. If a future need arises to show “which affiliate referred this CRM contact” inside a Portal view, expose it through a read-only adapter over the Module’s getReferralForContact — never by joining salesCommissions to ghlAffiliateCommissions in a query. Defer until there is a concrete consumer.
  5. Data that must never be joined: salesCommissions (FM-owned sales commission ledger, read by the Portal through its location binding) and ghlAffiliateCommissions (Module, CRM affiliate commissions) represent different money for different parties. clerkOrgId and ghlAffiliateId are different keyspaces and must never be equated.
  6. Do not revive canceled scope. Custom domains / DNS (FUND-1183), affiliate-domain linking (FUND-1084), and the data-import tool (FUND-1182) were explicitly canceled. Do not resurrect them under either system.

Safe migration path (if the Portal is eventually retired)

Staged, reversible, no data destruction until the end:
  1. Freeze: stop new Portal org signups (partnerOrganizations.create) and hide /partner-portal navigation behind a flag. Keep read access.
  2. Fix or fence the P0 first: either verify+lock every existing partnerOrganizations.locationId binding, or disable the earnings/dashboard reads, before any wind-down. A sunset does not excuse a live data-leak on deployed code.
  3. Stop the cron: remove the 15-minute Dub analytics-sync cron from vercel.json once links are no longer maintained (saves cron + Dub API traffic). Decide Dub link disposition (leave live, or delete via a one-off).
  4. Preserve history: salesCommissions is FM’s own commission ledger and may have accounting value independent of the Portal UI — keep the table even after the UI is gone (schema deletions are Out of Scope without Brock approval regardless).
  5. Decommission UI: remove app/partner-portal/* and components/partner-portal/* only after 1–4, in its own PR.
  6. Clerk: retire the Clerk org/billing configuration last, after confirming no other surface depends on it.

What to document

  • Linear: close/relabel the stale tickets found in the reconciliation — FUND-2033 (parent still Backlog while children shipped), FUND-1177 (Duplicate but merged), FUND-1261 (Done with 3/5 KRs canceled), FUND-2043 (Done but the live spike was completed only on 2026-07-12 — see PR #1198). Record whether FUND-1847’s cancellation actually executed.
  • CODEBASE.md: add a one-line pointer to this ADR next to the Portal / Partner Module schema-table listing so the two are never conflated again.

Consequences

  • The Portal’s P0 (partnerOrganizations.create unverified locationId) remains a live cross-tenant exposure on deployed code even though the product is paused. It is filed as the top P0 in the audit and needs an explicit decision: fix the ownership binding (+ backfill re-validation) or disable the Portal earnings/dashboard reads. It is not auto-fixed here because it touches auth/onboarding (Out of Scope without approval).
  • No code or schema changes in this ADR — documentation only.