> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myfundingmachine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Partner systems adr

# 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":

|                 | **Partner Module** (newer)                                                                                             | **Partner Portal** (older)                                                                                                                                 |
| --------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Routes          | `/partners`, `/admin/partners`                                                                                         | `/partner-portal/*`                                                                                                                                        |
| Auth            | CRM SSO / session actor (marketplace iframe)                                                                           | Clerk (external login)                                                                                                                                     |
| Identity key    | `ghlAffiliateId`                                                                                                       | `clerkOrgId` / `partnerOrganizations._id`                                                                                                                  |
| Data            | `ghlAffiliates`, `ghlAffiliateCampaigns`, `ghlAffiliateCommissions`, `ghlAffiliatePayouts`, `partnerReportingSettings` | `partnerOrganizations`, `partnerReferralLinks`, `salesCommissions` (FM-owned ledger read through the Portal's location binding), `organizationMemberships` |
| Source of truth | CRM Affiliate Manager (read) + CRM Workflows (enroll)                                                                  | Convex-native + Dub.co links + Clerk billing                                                                                                               |
| Audience        | CRM affiliate partners surfaced to operators/admins inside the CRM                                                     | External referral partners with their own white-label login                                                                                                |
| Built           | FUND-2039 → FUND-2047 (July 2026)                                                                                      | FUND-1038 → FUND-1296 (Mar–May 2026)                                                                                                                       |

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.**

   | Concept                         | Owner                | Key                                                                                                                                                                                            | Notes                                                                                                                                                       |
   | ------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
   | Affiliate identity              | **Module**           | `ghlAffiliateId`                                                                                                                                                                               | The CRM Affiliate Manager is the system of record                                                                                                           |
   | Partner *organization* identity | **Portal**           | `clerkOrgId`                                                                                                                                                                                   | A Clerk org; unrelated to an affiliate                                                                                                                      |
   | Referral links                  | **Portal**           | `partnerReferralLinks` + Dub.co                                                                                                                                                                | No Module equivalent                                                                                                                                        |
   | Referred contacts               | **Module**           | `commission.contactId` (CRM contact)                                                                                                                                                           | Query-time join, no link table                                                                                                                              |
   | Commissions                     | **split by concept** | Module: `ghlAffiliateCommissions` (from CRM). `salesCommissions` is the location-scoped Funding Machine sales commission ledger; the Portal reads it through `partnerOrganizations.locationId` | These are **different commission concepts** — never sum them together. `salesCommissions` remains FM-owned accounting history even if the Portal is retired |
   | Payouts                         | **Module**           | `ghlAffiliatePayouts`                                                                                                                                                                          | Portal has no payout ledger                                                                                                                                 |
   | Partner-facing reporting        | **Module**           | `partnerReportingSettings` + `analyticsScheduledReports.partnerId`                                                                                                                             | `partnerId` is always a `ghlAffiliateId`                                                                                                                    |
   | CRM location association        | **Module (trusted)** | verified session `locationId`                                                                                                                                                                  | The Portal's self-asserted `locationId` is the P0 — it is NOT a trustworthy binding                                                                         |

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.
