---
title: "Network and Marketplace"
description: "The Dub Network and Marketplace system powers affiliate program discovery, similarity scoring, partner ranking algorithms, workspace recruitment dashboards, public marketplace routing, and structur..."
last_updated: "2026-10-05T05:07:35.16912+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/network-and-marketplace"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/prisma/schema/network.prisma](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/network.prisma)
- [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts)
- [apps/web/app/ee/api/network/partners/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/network/partners/route.ts)
- [apps/web/scripts/dev/data.json](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/dev/data.json)
- [apps/web/ui/program-marketplace/program-marketplace-card.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/program-marketplace-card.tsx)
- [packages/ui/src/nav/content/program-marketplace.tsx](https://github.com/blade47/dub/blob/HEAD/packages/ui/src/nav/content/program-marketplace.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/page.tsx)
- [apps/web/lib/api/network/calculate-partner-ranking.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/network/calculate-partner-ranking.ts)
- [apps/web/app/api/callback/plain/partner/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/plain/partner/route.ts)
- [apps/web/ui/program-marketplace/marketplace-program-header-controls.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/marketplace-program-header-controls.tsx)
- [apps/web/lib/network/get-program-network-invite-email-defaults.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/network/get-program-network-invite-email-defaults.ts)
- [apps/web/ui/program-marketplace/program-marketplace-banner.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/program-marketplace-banner.tsx)
- [apps/web/app/ee/api/admin/partners/partnerId/shared-platforms/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/shared-platforms/route.ts)
- [apps/web/lib/api/partners/get-network-invites-usage.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/partners/get-network-invites-usage.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/network-upsell.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/network-upsell.tsx)
- [apps/web/app/ee/partners.dub.co/dashboard/profile/network-approval-guide.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/network-approval-guide.tsx)
- [apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts)
- [apps/web/ui/program-marketplace/external/marketplace-external-program-page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/external/marketplace-external-program-page.tsx)
- [apps/web/app/ee/partners.dub.co/auth-other/invite/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(auth-other)/invite/page.tsx)
- [apps/web/ui/program-marketplace/featured-program-card.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/featured-program-card.tsx)
- [apps/web/app/ee/partners.dub.co/dashboard/programs/invitations/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/invitations/page-client.tsx)
- [apps/web/lib/dub.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/dub.ts)
- [apps/web/ui/program-marketplace/marketplace-program-hero.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/marketplace-program-hero.tsx)
- [apps/web/ui/program-marketplace/pages/marketplace-programs-list-page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/pages/marketplace-programs-list-page.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/page-client.tsx)
- [apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-category-similarity.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/calculate-category-similarity.ts)
- [apps/web/app/sitemap.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/sitemap.ts)
- [apps/web/app/app.dub.co/marketplace/...segments/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/marketplace/%5B%5B...segments%5D%5D/page.tsx)
- [apps/web/app/ee/partners.dub.co/dashboard/programs/invitations/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/invitations/page.tsx)
- [packages/email/src/templates/broadcasts/program-marketplace-announcement.tsx](https://github.com/blade47/dub/blob/HEAD/packages/email/src/templates/broadcasts/program-marketplace-announcement.tsx)
</details>

## Overview

The Dub Network and Marketplace system powers affiliate program discovery, similarity scoring, partner ranking algorithms, workspace recruitment dashboards, public marketplace routing, and structured invitation lifecycles. It connects operators seeking high-performing affiliates with partners looking for relevant SaaS programs across multiple categories.

Sources: [apps/web/prisma/schema/network.prisma:1-40](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/network.prisma#L1-L40), [apps/web/lib/api/network/calculate-partner-ranking.ts:1-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/network/calculate-partner-ranking.ts#L1-L39)

The platform solves complex matching and recruitment challenges by utilizing automated cron pipelines for similarity evaluation, multi-factor ranking heuristics for discovery, comprehensive workspace control centers, and streamlined onboarding and application flows for partners.

Sources: [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:25-50](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L25-L50), [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:71-122](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/page-client.tsx#L71-L122)

## Program Similarity Calculation Engine

### Overview

The Program Similarity Calculation Engine runs as an automated cron pipeline executing once every 12 hours via `POST /api/cron/network/calculate-program-similarities`. It evaluates active affiliate programs in batches of 10 (`PROGRAMS_PER_BATCH = 10`), verifying QStash signatures and computing multi-dimensional similarity scores across shared categories, overlapping partners, and performance metrics.

Sources: [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:25-50](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L25-L50), [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:1-24](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L1-L24)

### Execution Pipeline and Call Chain

The similarity computation engine processes programs sequentially using pagination cursors and database transactions. The primary execution flow follows this strict call chain:

`POST` handler (`route.ts`) to `verifyQstashSignature()` to `calculateProgramSimilarity()` to `findNextProgram()` to `prisma.program.findMany()` to `Promise.all([calculateCategorySimilarity(), calculatePartnerSimilarity(), calculatePerformanceSimilarity()])` to `prisma.$transaction()` to `qstash.publishJSON()`

Sources: [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:30-50](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L30-L50), [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:52-113](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L52-L113), [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:168-214](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L168-L214)

> [!WARNING]
> Programs are only eligible for comparison if they are non-deactivated (`deactivatedAt: null`) and have either been added to the marketplace (`addedToMarketplaceAt: not: null`) or have the partner network enabled (`partnerNetworkEnabledAt: not: null`).

Sources: [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:64-82](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L64-L82)

### Scoring Algorithms and Weights

The final similarity score between two programs combines three distinct similarity sub-scores with fixed weighting coefficients:

Similarity Score equals `categoryScore` times 0.5 plus `partnerScore` times 0.3 plus `performanceScore` times 0.2.

If the calculated similarity score exceeds `PROGRAM_SIMILARITY_SCORE_THRESHOLD`, bidirectional records are pushed into the `ProgramSimilarity` table within a Prisma transaction, replacing previous entries for those program IDs.

Sources: [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:132-184](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L132-L184)

| Dimension | Weight | Calculation Method | Source File |
| --- | --- | --- | --- |
| Category Similarity | 0.5 | Jaccard similarity coefficient on `ProgramCategory` records (`sharedCount / totalUniqueCount`) | [apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-category-similarity.ts:4-43](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/calculate-category-similarity.ts#L4-L43) |
| Partner Similarity | 0.3 | Jaccard similarity coefficient on overlapping `ProgramEnrollment` partner IDs (`sharedPartnersCount / unionCount`) | [apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts:10-44](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts#L10-L44) |
| Performance Similarity | 0.2 | Comparative performance metrics evaluated concurrently via promise resolution | [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:122-130](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L122-L130) |

Sources: [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:132-135](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L132-L135), [apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-category-similarity.ts:4-43](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/calculate-category-similarity.ts#L4-L43), [apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts:10-44](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts#L10-L44)

> [!TIP]
> Both category and partner similarity return `0` immediately if the denominator (`totalUniqueCount` or `unionCount`) evaluates to zero, preventing division-by-zero exceptions during similarity calculations.

Sources: [apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-category-similarity.ts:38-41](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/calculate-category-similarity.ts#L38-L41), [apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts:39-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts#L39-L42)

### Database Schema and Design Trade-offs

The `ProgramSimilarity` model relies on a composite unique constraint and indexes to maintain fast lookup performance for similarity recommendations.

```prisma
model ProgramSimilarity {
  id                         String @id @default(cuid())
  programId                  String
  similarProgramId           String
  similarityScore            Float
  categorySimilarityScore    Float
  partnerSimilarityScore     Float
  performanceSimilarityScore Float

  program        Program @relation(fields: [programId], references: [id], onDelete: Cascade)
  similarProgram Program @relation("SimilarProgram", fields: [similarProgramId], references: [id], onDelete: Cascade)

  @@unique([programId, similarProgramId])
  @@index([programId, similarityScore])
  @@index(similarProgramId)
}
```

Sources: [apps/web/prisma/schema/network.prisma:25-40](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/network.prisma#L25-L40)

| Design Choice | Benefit | Cost |
| --- | --- | --- |
| Bidirectional pairing insertion | O(1) read performance when querying similar programs for any given program ID | Doubles storage volume in `ProgramSimilarity` for every qualified pair |
| QStash batch queueing (`PROGRAMS_PER_BATCH = 10`) | Prevents cron timeouts by distributing heavy calculations across iterative asynchronous tasks | Extends total pipeline completion time across multiple scheduled dispatches |
| Raw SQL for partner Jaccard aggregation | Executes partner overlap counts efficiently directly within database memory | Ties query logic directly to SQL syntax and database schema foreign keys |

Sources: [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L25), [apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:147-165](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/route.ts#L147-L165), [apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts:14-25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts#L14-L25), [apps/web/prisma/schema/network.prisma:25-40](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/network.prisma#L25-L40)

## Partner Network Ranking and Discovery

### Overview

The partner network API provides programmatic filtering, scoring heuristics, and ranking calculations for discoverable network partners. Workspace operators query available network partners via endpoints that evaluate program similarity scores (`similarityScore > PROGRAM_SIMILARITY_SCORE_THRESHOLD`), process query parameters, and execute ranking algorithms or listing filters depending on the requested status.

Sources: [apps/web/app/ee/api/network/partners/route.ts:16-56](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/network/partners/route.ts#L16-L56), [apps/web/lib/api/network/calculate-partner-ranking.ts:14-29](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/network/calculate-partner-ranking.ts#L14-L29)

### API Request Handling and Call-Chain Execution

When a GET request hits `/api/network/partners`, the endpoint executes a strict sequence of validation and database operations to fetch and format records.

The request processing follows this exact call chain:
1. `withWorkspace()` — Validates workspace authentication and extracts workspace context along with request search parameters.
2. `getDefaultProgramIdOrThrow()` — Resolves the default program identifier for the current workspace.
3. `prisma.program.findUniqueOrThrow()` — Fetches the target program record including its related `similarPrograms` filtered by `similarityScore` greater than `PROGRAM_SIMILARITY_SCORE_THRESHOLD`, ordered descending, taking up to 10 records.
4. `getNetworkPartnersQuerySchema.parse()` — Validates query parameters including `partnerIds`, `status`, `page`, `pageSize`, `country`, `starred`, `sortBy`, and `platform`.
5. Branching execution:
   - If `status` is **not** `"discover"`: executes `partnerNetworkListingWhere()`, queries `prisma.discoveredPartner.findMany()`, and maps results through `NetworkPartnerSchema.parse()`.
   - If `status` is `"discover"`: invokes `calculatePartnerRanking()` passing the structured parameters and similar programs list, then parses and formats the returned ranking array.

Sources: [apps/web/app/ee/api/network/partners/route.ts:17-132](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/network/partners/route.ts#L17-L132)

> [!WARNING]
> If `program.partnerNetworkEnabledAt` is null or undefined, the endpoint immediately throws a `DubApiError` with code `"forbidden"` and stops execution, preventing any partner queries from running on disabled programs.

Sources: [apps/web/app/ee/api/network/partners/route.ts:40-45](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/network/partners/route.ts#L40-L45)

### Partner Ranking Scoring Heuristics

For partners in the `"discover"` tab, `calculatePartnerRanking()` computes a composite score ranging from 0 to over 265 points. The scoring model aggregates priority bonuses, similarity performance, and program matches.

| Component | Point Range | Heuristic Details |
| --- | --- | --- |
| Trusted Partner Bonus | 200 points | Awarded to partners with `networkStatus = "trusted"` to ensure top placement |
| Similarity Score | 0–50 points | Sums weighted performance across similar programs where `similarityScore > 0.3` (consistency 20%, conversion rate 10%, LTV 15%, commissions 5%) |
| Program Match Score | 0–15 points | Awards 2 points per similar program the partner is enrolled in, capped at 15 points |

Sources: [apps/web/lib/api/network/calculate-partner-ranking.ts:18-34](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/network/calculate-partner-ranking.ts#L18-L34)

The sorting order of discovered partners is determined by `buildOrderByClause()`, which handles pinned or starred partners, platform subscriber counts, and relevance ranking.

```typescript
function buildOrderByClause({
  starred,
  sortBy,
  platform,
}: {
  starred?: boolean | null;
  sortBy?: "relevance" | "subscribers";
  platform?: PlatformType;
}) {
  if (starred === true) {
    return Prisma.sql`dp.starredAt DESC`;
  }

  if (sortBy === "subscribers" && platform) {
    return Prisma.sql`(
      SELECT COALESCE(MAX(pp_sort.subscribers), 0)
      FROM PartnerPlatform pp_sort
      WHERE pp_sort.partnerId = p.id
        AND pp_sort.type = ${platform}
        AND pp_sort.verifiedAt IS NOT NULL
    ) DESC, p.id ASC`;
  }

  return Prisma.sql`finalScore DESC, p.id ASC`;
}
```

Sources: [apps/web/lib/api/network/calculate-partner-ranking.ts:40-64](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/network/calculate-partner-ranking.ts#L40-L64)

### Shared Platform Identification

Administrators can inspect other network partners sharing the same verified platform identifiers via `/api/admin/partners/[partnerId]/shared-platforms`. The endpoint retrieves the target partner's verified platforms, normalizes website identifiers to domain names using `getDomainWithoutWWW()`, and queries matching records across other partner accounts.

Sources: [apps/web/app/ee/api/admin/partners/partnerId/shared-platforms/route.ts:10-87](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/shared-platforms/route.ts#L10-L87)

> [!NOTE]
> Website platform matching uses a `contains` clause on the extracted website domain rather than an exact identifier match, followed by a strict verification check to ensure domain equality and filter out partial string collisions.

Sources: [apps/web/app/ee/api/admin/partners/partnerId/shared-platforms/route.ts:39-51](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/shared-platforms/route.ts#L39-L51), [apps/web/app/ee/api/admin/partners/partnerId/shared-platforms/route.ts:97-103](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/shared-platforms/route.ts#L97-L103)

## Workspace Partner Network UI

### Overview

The dashboard interface for the partner network provides program operators with tools to discover, filter, inspect, and evaluate potential partners. Built around `ProgramPartnerNetworkPageClient`, the dashboard manages state across multiple tabs, platform filters, star rankings, and partner detail sheets.

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:71-391](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/page-client.tsx#L71-L391)

### Dashboard Tabs and State Management

The interface organizes partner management into three main tabs: `discover`, `invited`, and `recruited`. Operators can also access an ignored variant view. State synchronization relies heavily on URL query parameters handled by `useRouterStuff()` and SWR data fetching.

| Tab ID | Label | Description |
| --- | --- | --- |
| `discover` | Discover | Displays prospective network partners available for recruitment |
| `invited` | Invited | Lists partners who have received an invitation to the program |
| `recruited` | Recruited | Displays partners who have successfully joined the program |

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:39-52](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/page-client.tsx#L39-L52), [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:67-81](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/page-client.tsx#L67-L81)

> [!NOTE]
> When switching between tabs via `queryParams`, parameters such as `page`, `starred`, and `sortBy` are automatically cleared to prevent invalid filter states across different list categories.

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:201-206](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/page-client.tsx#L201-L206)

### Platform Filters and Toggle Options

Within the `discover` tab, operators can filter potential partners by their connected social platforms or website presence using a toggle group component. 

| Platform Value | Associated Icon |
| --- | --- |
| `all` | User |
| `website` | Globe |
| `youtube` | YouTube |
| `twitter` | Twitter |
| `linkedin` | LinkedIn |
| `instagram` | Instagram |
| `tiktok` | TikTok |

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:54-65](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/page-client.tsx#L54-L65), [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:224-259](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/page-client.tsx#L224-L259)

### Partner Inspection and Navigation Walkthrough

Operators can click on any partner card to open a detailed sheet (`NetworkPartnerSheet`). The component handles deep-linking via search parameters and computes adjacent navigation items for quick traversal.

The inspection lifecycle executes through the following sequence:
1. `useEffect()` inspects the URL search parameters for a `partnerId`. If present, it updates `detailsSheetState` to open the sheet.
2. `useCurrentPartner()` evaluates whether the target partner exists in the loaded array (`partners`); if not, it fetches the specific partner via SWR from `/api/network/partners`.
3. `useMemo()` calculates `previousPartnerId` and `nextPartnerId` by finding the index of the current partner within the loaded list.
4. Triggering `onPrevious` or `onNext` invokes `queryParams({ set: { partnerId: previousPartnerId } })`, updating the active sheet view without closing the container.

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:133-183](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/page-client.tsx#L133-L183), [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:393-425](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/page-client.tsx#L393-L425)

> [!WARNING]
> If a workspace lacks enterprise permissions, the network view falls back to `NetworkUpsell`, rendering a preview cell list and prompting the user to upgrade to Enterprise or contact sales.

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/program/network/network-upsell.tsx:9-44](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/network/network-upsell.tsx#L9-L44)

## Marketplace Directory and Program Display

### Overview

The public and authenticated marketplace routing system exposes program directories, category pages, individual program views, and promotional UI components. Routing is governed by dynamic Next.js segments under `/marketplace/...segments/page.tsx`, which inspects URL segments to resolve individual network programs or category filters. Public promotional elements include `ProgramMarketplaceBanner` and `ProgramMarketplaceCard`, which check partner profile SWR hooks and promo status states before rendering animated background grids and floating logo containers.

Sources: [apps/web/app/app.dub.co/marketplace/...segments/page.tsx:14-67](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/marketplace/%5B%5B...segments%5D%5D/page.tsx#L14-L67), [apps/web/ui/program-marketplace/program-marketplace-card.tsx:12-20](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/program-marketplace-card.tsx#L12-L20), [apps/web/ui/program-marketplace/program-marketplace-banner.tsx:12-18](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/program-marketplace-banner.tsx#L12-L18)

### Marketplace Routing and Metadata Generation

Dynamic marketplace routing resolves parameters through asynchronous `generateMetadata` functions and page components. When visitors navigate to marketplace URLs, the segment matcher evaluates whether the path requests the all-programs list, a specific program slug, or a category prefix (`/c/`).

The metadata generation sequence executes through the following validation checks:
1. `generateMetadata()` reads `params` to extract the `segments` array, defaulting to an empty list for the marketplace home root.
2. If `segments.length === 1` and equals `"all"`, it sets the title to top SaaS affiliate programs for the current year.
3. If `segments.length === 1` and matches a non-reserved string, it invokes `getNetworkProgram({ slug: segments[0] })` to fetch program details, overriding title, description, and OG image fields.
4. If `segments.length === 2` and `segments[0] === "c"`, it scans `Category` enums to match `segments[1]`, resolving category-specific labels and API OpenGraph endpoints.

Sources: [apps/web/app/app.dub.co/marketplace/...segments/page.tsx:14-60](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/marketplace/%5B%5B...segments%5D%5D/page.tsx#L14-L60)

> [!NOTE]
> When `domain` matches `PARTNERS_HOSTNAMES`, the sitemap generator queries Prisma for programs containing published lander data (`landerData: { not: Prisma.AnyNull }` and `landerPublishedAt: { not: null }`), outputting custom partner lander URLs.

Sources: [apps/web/app/sitemap.ts:20-44](https://github.com/blade47/dub/blob/HEAD/apps/web/app/sitemap.ts#L20-L44)

### Program Display Components and Hero Layouts

Programs are rendered across discovery grids and detail views using specialized components such as `MarketplaceProgramHero`, `FeaturedProgramCard`, and `MarketplaceProgramsListPage`. 

| Component File | Primary Role | Key Props & Dependencies |
| --- | --- | --- |
| `marketplace-program-hero.tsx` | Renders top banner image, avatar, title, description, categories, website links, and application slots | `program`, `applySlot`, `className` |
| `featured-program-card.tsx` | Displays featured program cards with dynamic background tinting and reward summaries | `program`, `externalMarketplace`, `colorIndex` |
| `marketplace-programs-list-page.tsx` | Manages list fetching, SWR caching, pagination controls, and empty-state filtering | None (Client Component) |

Sources: [apps/web/ui/program-marketplace/marketplace-program-hero.tsx:13-21](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/marketplace-program-hero.tsx#L13-L21), [apps/web/ui/program-marketplace/featured-program-card.tsx:35-43](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/featured-program-card.tsx#L35-L43), [apps/web/ui/program-marketplace/pages/marketplace-programs-list-page.tsx:19-42](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/pages/marketplace-programs-list-page.tsx#L19-L42)

> [!IMPORTANT]
> `FeaturedProgramCard` and `MarketplaceProgramHero` use `useImageAccentColor` to extract dominant color tints from program header images or logos, falling back to predefined palette arrays (`FEATURED_CARD_BACKGROUNDS`) if image extraction is unavailable.

Sources: [apps/web/ui/program-marketplace/featured-program-card.tsx:14-52](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/featured-program-card.tsx#L14-L52), [apps/web/ui/program-marketplace/marketplace-program-hero.tsx:23-26](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/marketplace-program-hero.tsx#L23-L26)

### Sitemap Generation Logic

The sitemap generator in `apps/web/app/sitemap.ts` inspects incoming request headers to determine host environments and construct matching XML sitemap entries. 

```typescript
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const headersList = await headers();
  let domain = headersList.get("host") as string;

  if (domain === "dub.localhost:8888") {
    domain = SHORT_DOMAIN;
  }

  if (PARTNERS_HOSTNAMES.has(domain)) {
    const programs = await prisma.program.findMany({
      where: { groups: { some: { slug: "default", landerData: { not: Prisma.AnyNull }, landerPublishedAt: { not: null } } } },
      orderBy: { slug: "asc" },
    });
    return programs.map((program) => ({
      url: `https://partners.dub.co/${program.slug}`,
      lastModified: new Date(),
    }));
  }

  const entries: MetadataRoute.Sitemap = [{ url: `https://${domain}`, lastModified: new Date() }];

  if (isAppHostname(domain)) {
    const marketplacePrograms = await prisma.program.findMany({
      where: { addedToMarketplaceAt: { not: null } },
      select: { slug: true, updatedAt: true },
      orderBy: { slug: "asc" },
    });

    entries.push(
      { url: "https://dub.co/marketplace", lastModified: new Date() },
      { url: `https://dub.co${getMarketplaceAllHref()}`, lastModified: new Date() },
      ...Object.values(Category).map((category) => ({
        url: `https://dub.co${getMarketplaceCategoryHref(category)}`,
        lastModified: new Date(),
      })),
      ...marketplacePrograms.map((program) => ({
        url: `https://dub.co/marketplace/${program.slug}`,
        lastModified: program.updatedAt,
      })),
    );
  }

  return entries;
}
```

Sources: [apps/web/app/sitemap.ts:11-90](https://github.com/blade47/dub/blob/HEAD/apps/web/app/sitemap.ts#L11-L90)

## Network Invitation Lifecycle and Quotas

### Overview

Managing network invitations involves monitoring monthly workspace quotas, orchestrating the partner acceptance lifecycle, and supplying clean default parameters for outbound invitation emails. Workspace invitation limits are determined by aggregating discovered partner metrics against billing cycle start dates, while partner acceptance workflows handle asynchronous API submissions, session refreshes, and cache invalidation.

Sources: [apps/web/lib/api/partners/get-network-invites-usage.ts:5-30](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/partners/get-network-invites-usage.ts#L5-L30), [apps/web/app/ee/partners.dub.co/auth-other/invite/page.tsx:18-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(auth-other)/invite/page.tsx#L18-L42), [apps/web/lib/network/get-program-network-invite-email-defaults.ts:13-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/network/get-program-network-invite-email-defaults.ts#L13-L27)

### Monthly Invite Usage Tracking

Workspace quota utilization for network invitations is calculated in `getNetworkInvitesUsage` via Prisma database aggregation. The query counts `DiscoveredPartner` records tied to the workspace's programs where either the invitation timestamp or messaging timestamp falls after the current billing cycle start date.

```typescript
export async function getNetworkInvitesUsage(
  workspace: Pick<Project, "id" | "billingCycleStart">,
) {
  const invites = await prisma.discoveredPartner.aggregate({
    _count: true,
    where: {
      program: {
        workspaceId: workspace.id,
      },
      OR: [
        {
          invitedAt: {
            gt: getBillingStartDate(workspace.billingCycleStart),
          },
        },
        {
          messagedAt: {
            gt: getBillingStartDate(workspace.billingCycleStart),
          },
        },
      ],
    },
  });

  return invites._count;
}
```

Sources: [apps/web/lib/api/partners/get-network-invites-usage.ts:5-30](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/partners/get-network-invites-usage.ts#L5-L30)

> [!NOTE]
> `getNetworkInvitesUsage` tracks usage by evaluating both `invitedAt` and `messagedAt` timestamps against `getBillingStartDate(workspace.billingCycleStart)`. A partner interaction triggers quota consumption if either event occurred within the active billing period.

Sources: [apps/web/lib/api/partners/get-network-invites-usage.ts:14-25](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/partners/get-network-invites-usage.ts#L14-L25)

### Partner Acceptance Workflow

The partner invite acceptance page (`AcceptPartnerInvitePage`) manages user onboarding onto partner profiles through client-side state handling and SWR cache mutations. 

```mermaid
sequenceDiagram
    participant User
    participant Page as AcceptPartnerInvitePage
    participant API as /api/partner-profile/invites/accept
    participant Session as NextAuth Session
    participant SWR as SWR Cache

    User->>Page: Click "Accept invite"
    Page->>API: POST /api/partner-profile/invites/accept
    alt Response OK
        API-->>Page: Success
        Page->>Session: refreshSession()
        Page->>SWR: mutatePrefix("/api/partner-profile")
        Page->>User: router.replace("/programs") & toast.success()
    else Response Error
        API-->>Page: Error JSON
        Page->>User: setAccepting(false) & toast.error()
    end
```

Sources: [apps/web/app/ee/partners.dub.co/auth-other/invite/page.tsx:18-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(auth-other)/invite/page.tsx#L18-L42)

> [!WARNING]
> If a user navigates to the invite page while already associated with an active partner profile (`!loading && partner`), the client immediately executes `router.replace("/programs")` and halts rendering.

Sources: [apps/web/app/ee/partners.dub.co/auth-other/invite/page.tsx:44-48](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(auth-other)/invite/page.tsx#L44-L48)

### Invitation Email Defaults and Partner Name Sanitization

Outbound email generation relies on helper utilities in `get-program-network-invite-email-defaults.ts` to sanitize recipient names. Because some network partners have raw email addresses stored in place of names, `getUsableNetworkPartnerName` checks for the presence of the `@` symbol and strips invalid entries.

| Utility Function | Input Parameter | Return Type | Behavior / Sanitization Rule |
| --- | --- | --- | --- |
| `getUsableNetworkPartnerName` | `name?: string \| null` | `string \| null` | Trims input; returns `null` if empty or if string contains `@`. |
| `getNetworkPartnerDisplayName` | `name?: string \| null` | `string` | Delegates to `getUsableNetworkPartnerName`; falls back to `"there"`. |
| `getProgramNetworkInviteEmailDefaults` | `{ programName: string, partnerName?: string \| null }` | `{ subject, title, body }` | Constructs template strings using the sanitized partner display name. |

Sources: [apps/web/lib/network/get-program-network-invite-email-defaults.ts:3-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/network/get-program-network-invite-email-defaults.ts#L3-L27)

> [!TIP]
> When generating program invite copy, `getNetworkPartnerDisplayName` ensures that email addresses stored as partner names never leak into outbound notification copy, substituting a friendly fallback like `"there"` automatically.

Sources: [apps/web/lib/network/get-program-network-invite-email-defaults.ts:1-11](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/network/get-program-network-invite-email-defaults.ts#L1-L11)

## Partner Profile and Application Flow

### Overview

The partner profile and application flow govern how partners onboard onto the network, fulfill requirements, submit network applications, and interact with support through Plain webhooks. Partner progression relies on checklist completion, status validation, and state-driven UI controls.

Sources: [apps/web/app/ee/partners.dub.co/dashboard/profile/network-approval-guide.tsx:40-148](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/network-approval-guide.tsx#L40-L148), [apps/web/ui/program-marketplace/marketplace-program-header-controls.tsx:49-189](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/marketplace-program-header-controls.tsx#L49-L189)

### Partner Onboarding Checklist and Approval Guide

The `NetworkApprovalGuide` component renders the interactive onboarding workflow for partners inside the partner dashboard. It tracks checklist progress, handles draft submissions via `submitNetworkProfileAction`, and dynamically reflects partner approval states.

| Network Status | Badge Variant | Icon | Label |
| --- | --- | --- | --- |
| `submitted` | `pending` | `CircleHalfDottedClock` | Pending approval |
| `rejected` | `error` | `CircleXmark` | Rejected |
| `approved` / `trusted` | `success` | `CircleCheck` | Approved |

Sources: [apps/web/app/ee/partners.dub.co/dashboard/profile/network-approval-guide.tsx:22-38](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/network-approval-guide.tsx#L22-L38), [apps/web/app/ee/partners.dub.co/dashboard/profile/network-approval-guide.tsx:110-127](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/network-approval-guide.tsx#L110-L127)

> [!NOTE]
> If a partner's network status is evaluated as `trusted`, the approval guide explicitly normalizes it to display as `approved` using the standard success badge variant.

Sources: [apps/web/app/ee/partners.dub.co/dashboard/profile/network-approval-guide.tsx:110-113](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/network-approval-guide.tsx#L110-L113)

### Program Application Controls and Eligibility Logic

The `MarketplaceProgramHeaderControls` component manages program enrollment actions, toggling between invite acceptance, dashboard redirection, and application sheet triggers based on current enrollment status.

```mermaid
graph TD
    A[Program Enrollment Status] -->|invited| B[AcceptInviteButton]
    A[Approved] -->|approved| C[View Dashboard Link]
    A[Other / None] -->|default| D[ApplyButton / Sheet]
```

Sources: [apps/web/ui/program-marketplace/marketplace-program-header-controls.tsx:30-47](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/marketplace-program-header-controls.tsx#L30-L47)

> [!WARNING]
> Program application buttons remain disabled if checklist tasks are incomplete, the partner network status is outside `approved` or `trusted`, or the applicant fails custom program requirements evaluated by `evaluateApplicationRequirements`.

Sources: [apps/web/ui/program-marketplace/marketplace-program-header-controls.tsx:104-170](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/marketplace-program-header-controls.tsx#L104-L170)

### Support Integrations via Plain Webhooks

The Plain integration route (`/api/callback/plain/partner`) authenticates incoming webhooks using the `X-Plain-Webhook-Secret` header, resolves partner profiles via database lookups, and hydrates Plain customer cards with financial and account identifiers.

```typescript
export async function POST(req: NextRequest) {
  const token = req.headers.get("X-Plain-Webhook-Secret");
  if (token !== process.env.PLAIN_WEBHOOK_SECRET) {
    return new Response("Unauthorized", { status: 401 });
  }

  let { customer } = plainCallbackSchema.parse(await req.json());

  if (!customer.externalId) {
    const user = await prisma.user.findUnique({
      where: { email: customer.email },
    });
    if (!user || !user.email) {
      return NextResponse.json({
        cards: [{ key: "partner", components: [plainEmptyContainer("No user found.")] }],
      });
    }
    customer.externalId = user.id;
    await upsertPlainCustomer({ id: user.id, name: user.name, email: user.email });
  }
  // ...fetches partner profile and renders customer view cards
}
```

Sources: [apps/web/app/api/callback/plain/partner/route.ts:21-55](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/plain/partner/route.ts#L21-L55)

> [!TIP]
> When a webhook lacks an `externalId`, the route automatically queries the `User` table by email, provisions the external ID link, and calls `upsertPlainCustomer` before attempting partner profile matching.

Sources: [apps/web/app/api/callback/plain/partner/route.ts:31-55](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/plain/partner/route.ts#L31-L55)

## Related

- [Partner Program Management](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/partner-program-management)
- [Partner Portal and Onboarding](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/partner-portal-and-onboarding)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
