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.createbinds an org to a caller-supplied CRMlocationIdwith 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
- 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”).
- 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).
-
Ownership of each concept is single-system. Do not cross the streams.
-
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 joiningsalesCommissionstoghlAffiliateCommissionsin a query. Defer until there is a concrete consumer. -
Data that must never be joined:
salesCommissions(FM-owned sales commission ledger, read by the Portal through its location binding) andghlAffiliateCommissions(Module, CRM affiliate commissions) represent different money for different parties.clerkOrgIdandghlAffiliateIdare different keyspaces and must never be equated. - 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:- Freeze: stop new Portal org signups (
partnerOrganizations.create) and hide/partner-portalnavigation behind a flag. Keep read access. - Fix or fence the P0 first: either verify+lock every existing
partnerOrganizations.locationIdbinding, or disable the earnings/dashboard reads, before any wind-down. A sunset does not excuse a live data-leak on deployed code. - Stop the cron: remove the 15-minute Dub analytics-sync cron from
vercel.jsononce links are no longer maintained (saves cron + Dub API traffic). Decide Dub link disposition (leave live, or delete via a one-off). - Preserve history:
salesCommissionsis 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). - Decommission UI: remove
app/partner-portal/*andcomponents/partner-portal/*only after 1–4, in its own PR. - 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.createunverifiedlocationId) 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.

