Relevant source files
The following files were used as context for generating this wiki page:
- apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts
- packages/ui/src/nav/content/program-marketplace.tsx
- apps/web/lib/api/network/calculate-partner-ranking.ts
- apps/web/prisma/schema/network.prisma
- apps/web/ui/program-marketplace/program-marketplace-card.tsx
- apps/web/app/ee/api/track/application/route.ts
- apps/web/app/ee/api/network/partners/route.ts
- apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-performance-similarity.ts
- apps/web/ui/program-marketplace/pages/marketplace-programs-list-page.tsx
- apps/web/app/app.dub.co/marketplace/...segments/page.tsx
- apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts
- apps/web/app/ee/admin.dub.co/dashboard/programs/page.tsx
- apps/web/lib/fetchers/get-marketplace-programs-summary.ts
- apps/web/ui/program-marketplace/featured-program-card.tsx
- apps/web/ui/program-marketplace/program-marketplace-banner.tsx
- apps/web/ui/program-marketplace/pages/marketplace-home-page.tsx
- apps/web/app/api/og/program/categories/route.tsx
- apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-category-similarity.ts
- apps/web/ui/program-marketplace/external/marketplace-external-program-page.tsx
- apps/web/app/ee/api/marketplace/programs/route.ts
- apps/web/ui/program-marketplace/program-card.tsx
- apps/web/app/sitemap.ts
- apps/web/ui/program-marketplace/marketplace-program-hero.tsx
- apps/web/ui/program-marketplace/external/marketplace-external-list-page.tsx
- apps/web/app/ee/partners.dub.co/dashboard/programs/page-client.tsx
- apps/web/app/app.dub.co/dashboard/slug/ee/program/program-empty-state.tsx
- apps/web/lib/fetchers/get-public-network-programs.ts
- apps/web/app/app.dub.co/onboarding/onboarding/steps/program/page.tsx
- apps/web/app/ee/api/admin/partners/partnerId/shared-platforms/route.ts
- apps/web/app/ee/admin.dub.co/dashboard/programs/recent/page.tsx
Overview
The Program Marketplace serves as Dub's centralized directory for discovering, exploring, and applying to network affiliate and referral programs. It connects prospective affiliates with top SaaS programs through public listings, category filtering, and targeted search capabilities. By bridging public-facing discovery directories with internal workspace tooling, the marketplace enables administrators to curate featured collections and metadata while giving partners streamlined access to program enrollments and conversion tracking.
Sources: apps/web/app/app.dub.co/marketplace/...segments/page.tsx:22-39, apps/web/lib/fetchers/get-public-network-programs.ts:26-96, apps/web/app/ee/admin.dub.co/dashboard/programs/page.tsx:27-37
Public Marketplace Data Fetching
Server Fetchers and Prisma Queries
Discoverable network programs are retrieved through server-side fetchers powered by Prisma and cached via React's cache utility. The primary data retrieval function, getPublicNetworkPrograms, accepts query parameters including category, rewardType, featured, search, sorting fields (popularity, recency), pagination limits, and sort orders. Programs are filtered to include only those where addedToMarketplaceAt is not null. When search queries are present, Prisma evaluates an OR condition matching program name, slug, domain, url, or description. Reward type filtering evaluates the default partner group (DEFAULT_PARTNER_GROUP.slug) for specific relation IDs such as saleRewardId, leadRewardId, clickRewardId, or discountId.
export const getPublicNetworkPrograms = cache(
async (
params: Partial<z.infer<typeof getPublicNetworkProgramsQuerySchema>> = {},
) => {
const {
category,
rewardType,
featured,
search,
sortBy = "popularity",
sortOrder = "desc",
page = 1,
pageSize = PROGRAM_NETWORK_MAX_PAGE_SIZE,
} = params;
const programs = await prisma.program.findMany({
where: {
addedToMarketplaceAt: {
not: null,
},
...(featured && {
featuredOnMarketplaceAt: {
not: null,
},
}),
...(search && {
OR: [
{ name: { contains: search } },
{ slug: { contains: search } },
{ domain: { contains: search } },
{ url: { contains: search } },
{ description: { contains: search } },
],
}),
...(category && {
categories: {
some: {
category,
},
},
}),
...(rewardType && {
groups: {
some: {
slug: DEFAULT_PARTNER_GROUP.slug,
...(rewardType === "sale" && {
saleRewardId: { not: null },
}),
...(rewardType === "lead" && {
leadRewardId: { not: null },
}),
...(rewardType === "click" && {
clickRewardId: { not: null },
}),
...(rewardType === "discount" && {
discountId: { not: null },
}),
},
},
}),
},
include: {
groups: {
where: {
slug: DEFAULT_PARTNER_GROUP.slug,
},
include: {
clickReward: true,
leadReward: true,
saleReward: true,
referralReward: true,
discount: true,
},
},
categories: true,
},
orderBy:
sortBy === "popularity"
? { marketplaceRanking: "asc" }
: {Sources: apps/web/lib/fetchers/get-public-network-programs.ts:11-96
Summary summaries for marketplace home pages are fetched via getMarketplaceProgramsSummary, which queries marketplace-enabled programs, groups them into featured sets, most popular lists, recent additions, and category rows according to MARKETPLACE_HOME_CATEGORIES.
export const getMarketplaceProgramsSummary = cache(async () => {
const programs = await prisma.program.findMany({
where: {
addedToMarketplaceAt: {
not: null,
},
},
include: programInclude,
});
const programMeta = programs.map(toProgramMeta);
const usedIds = new Set<string>();
const selectRow = (
candidates: ProgramRecord[],
sortFn: (a: ProgramRecord, b: ProgramRecord) => number,
) =>
selectPrograms(
candidates,
usedIds,
MARKETPLACE_HOME_ROW_PAGE_SIZE,
sortFn,
).map(formatNetworkProgram);
const featuredPrograms = programs
.filter((program) => program.featuredOnMarketplaceAt)
.sort(() => Math.random() - 0.5)
.map(formatNetworkProgram);
const mostPopular = selectRow(programs, byMarketplaceRanking);
const newPrograms = selectPrograms(
programs,
new Set(),
MARKETPLACE_HOME_ROW_PAGE_SIZE,
byRecency,
).map(formatNetworkProgram);
const categoryRows = selectCategoryRows(programMeta, usedIds);
const categories = Object.fromEntries(
Object.values(Category).map((category) => [
category,
(categoryRows.get(category) ?? []).map(formatNetworkProgram),
]),
);
return MarketplaceProgramsSummarySchema.parse({
featuredPrograms,
mostPopular,
newPrograms,
categories,
});
});Sources: apps/web/lib/fetchers/get-marketplace-programs-summary.ts:146-197
Note
Reward extraction logic inspects program.groups[0], collecting click, lead, and sale rewards into an array while attaching the associated discount object. Programs lacking groups return empty reward arrays.
Sources: apps/web/lib/fetchers/get-public-network-programs.ts:98-112
Public API Routes and Edge Caching
The public marketplace HTTP endpoint exposes filtered and searched program lists at /api/marketplace/programs. The route handler parses incoming URL search parameters using parsePublicMarketplaceQuery before delegating execution to getPublicNetworkPrograms.
export async function GET(req: Request) {
const searchParams = Object.fromEntries(new URL(req.url).searchParams);
const params = parsePublicMarketplaceQuery(searchParams);
const programs = await getPublicNetworkPrograms(params);
return NextResponse.json(programs);
}Sources: apps/web/app/ee/api/marketplace/programs/route.ts:9-16
Tip
Route responses are configured with an edge revalidation window of 3,600 seconds (revalidate = 3600), caching filtered directory listings publicly at the edge without partner-scoped session overhead.
Sources: apps/web/app/ee/api/marketplace/programs/route.ts:5-6
Program Categories
Network programs can be categorized across thirteen industry domains defined by the Category enum in the Prisma schema.
| Category Enum Value | Description / Domain |
|---|---|
AI | Artificial intelligence applications and tools |
Development | Developer tools and engineering infrastructure |
Design | Design systems, assets, and creative software |
Productivity | Workflow enhancement and productivity applications |
Finance | Financial technology, banking, and accounting |
Marketing | Marketing automation, SEO, and growth tools |
Ecommerce | E-commerce platforms and merchant tooling |
Security | Cybersecurity, identity management, and compliance |
Education | Learning management systems and educational resources |
Health | Healthtech, fitness, and wellness applications |
Consumer | Consumer-facing software and hardware services |
Support | Customer support, helpdesk, and ticketing systems |
enum Category {
AI
Development
Design
Productivity
Finance
Marketing
Ecommerce
Security
Education
Health
Consumer
Support
}
Sources: apps/web/prisma/schema/network.prisma:1-14
Program Similarity Computation Engine
Scheduled Execution and Batching Strategy
The network similarity engine operates via a scheduled QStash cron job running every 12 hours (0 */12 * * *) at the endpoint /api/cron/network/calculate-program-similarities. The handler validates incoming QStash signatures, parses request payloads using Zod with currentProgramId and comparisonBatchCursor parameters, and processes programs in batches of 10 (PROGRAMS_PER_BATCH).
const payloadSchema = z.object({
currentProgramId: z
.string()
.optional()
.describe("Current program being compared."),
comparisonBatchCursor: z
.string()
.optional()
.describe("Cursor for programs to compare against."),
});
const PROGRAMS_PER_BATCH = 10;
// This route is used to calculate program similarities in the network
// Runs once every 12 hours (0 */12 * * *)
// POST /api/cron/network/calculate-program-similarities
export async function POST(req: Request) {Sources: apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:14-30
Programs are targeted for comparison if they are not deactivated (deactivatedAt: null) and are visible in either the marketplace (addedToMarketplaceAt is not null) or the partner network (partnerNetworkEnabledAt is not null). When a batch finishes processing, the job automatically schedules the next batch or advances to the subsequent target program via QStash JSON publishing.
const programs = await prisma.program.findMany({
where: {
id: {
gt: currentProgram.id,
},
deactivatedAt: null,
OR: [
{
addedToMarketplaceAt: {
not: null,
},
},
{
partnerNetworkEnabledAt: {
not: null,
},
},
],
},
...(comparisonBatchCursor && {
cursor: {
id: comparisonBatchCursor,
},
}),
skip: comparisonBatchCursor ? 1 : 0,
orderBy: {
id: "asc",
},
take: PROGRAMS_PER_BATCH,
select: {
id: true,
name: true,
},
});Sources: apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:64-97
Note
Program pairs resulting in a combined similarity score exceeding PROGRAM_SIMILARITY_SCORE_THRESHOLD are persisted bidirectionally (programId and similarProgramId entries) within a Prisma transaction after purging stale records for the batch.
Sources: apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:147-183
Multi-Dimensional Similarity Metrics
The final similarity score is computed as a weighted aggregate of three distinct vector and set-based dimensions: category similarity (weighted at 50%), partner similarity (weighted at 30%), and performance similarity (weighted at 20%).
const [
categorySimilarityScore,
partnerSimilarityScore,
performanceSimilarityScore,
] = await Promise.all([
calculateCategorySimilarity(program1.id, program2.id),
calculatePartnerSimilarity(program1.id, program2.id),
calculatePerformanceSimilarity(program1.id, program2.id),
]);
const similarityScore =
categorySimilarityScore * 0.5 +
partnerSimilarityScore * 0.3 +
performanceSimilarityScore * 0.2;
Sources: apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:122-136
| Similarity Dimension | Mathematical Approach | Implementation File | Weight |
|---|---|---|---|
| Category Similarity | Jaccard Similarity | calculate-category-similarity.ts | 0.5 |
| Partner Similarity | Jaccard Similarity | calculate-partner-similarity.ts | 0.3 |
| Performance Similarity | Cosine Similarity | calculate-performance-similarity.ts | 0.2 |
const similarityScore =
categorySimilarityScore * 0.5 +
partnerSimilarityScore * 0.3 +
performanceSimilarityScore * 0.2;Sources: apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:132-135
Component Algorithm Implementation
Category and partner similarities utilize Jaccard coefficients calculated from database relations. Category similarity compares distinct assigned categories via prisma.programCategory, while partner similarity executes a raw SQL join across ProgramEnrollment records to evaluate shared affiliate overlap.
export async function calculateCategorySimilarity(
program1Id: string,
program2Id: string,
): Promise<number> {
const [categories1, categories2] = await Promise.all([
prisma.programCategory.findMany({
where: {
programId: program1Id,
},
select: {
category: true,
},
}),
prisma.programCategory.findMany({
where: {
programId: program2Id,
},
select: {
category: true,
},
}),
]);
const categories1Set = new Set(categories1.map(({ category }) => category));
const categories2Set = new Set(categories2.map(({ category }) => category));
const sharedCount = [...categories1Set].filter((c) =>
categories2Set.has(c),
).length;
const totalUniqueCount =
categories1Set.size + categories2Set.size - sharedCount;
if (totalUniqueCount === 0) {
return 0;
}
return sharedCount / totalUniqueCount;
}Performance similarity builds vectors from averaged enrollment metrics across five specific keys: totalClicks, totalLeads, totalConversions, totalSales, and totalSaleAmount. It then computes the cosine similarity between the resulting vectors for the two programs.
const METRIC_KEYS = [
"totalClicks",
"totalLeads",
"totalConversions",
"totalSales",
"totalSaleAmount",
] as const;export async function calculatePerformanceSimilarity(
program1Id: string,
program2Id: string,
): Promise<number> {
const [performance1, performance2] = await Promise.all([
prisma.programEnrollment.aggregate({
where: {
programId: program1Id,
},
_avg: {
totalClicks: true,
totalLeads: true,
totalSales: true,
totalConversions: true,
totalSaleAmount: true,
},
}),
prisma.programEnrollment.aggregate({
where: {
programId: program2Id,
},
_avg: {
totalClicks: true,
totalLeads: true,
totalSales: true,
totalConversions: true,
totalSaleAmount: true,
},
}),
]);
const program1Vector = METRIC_KEYS.map((key) => performance1._avg[key] ?? 0);
const program2Vector = METRIC_KEYS.map((key) => performance2._avg[key] ?? 0);
const dotProduct = program1Vector.reduce(
(sum, val, i) => sum + val * program2Vector[i],
0,
);
const magnitude1 = Math.sqrt(
program1Vector.reduce((sum, val) => sum + val ** 2, 0),
);
const magnitude2 = Math.sqrt(
program2Vector.reduce((sum, val) => sum + val ** 2, 0),
);
if (magnitude1 === 0 || magnitude2 === 0) {
return 0;
}
return dotProduct / (magnitude1 * magnitude2);
}Partner Ranking and Discovery Scoring
Discoverability and Ranking Algorithm
Workspace partner search operations evaluate eligibility and calculate scores via calculatePartnerRanking. Partners must meet baseline criteria: their network status must be either "approved" or "trusted", their click-to-conversion rate must be strictly below 1, they must not have an active enrollment (enrolled.id IS NULL), and they must not be marked as ignored (dp.ignoredAt IS NULL).
const conditions: Prisma.Sql[] = [
Prisma.sql`p.networkStatus IN ("approved", "trusted")`,
Prisma.sql`COALESCE(pe.clickToConversionRate, 0) < 1`,
Prisma.sql`(dp.ignoredAt IS NULL OR dp.id IS NULL)`,
Prisma.sql`enrolled.id IS NULL`,
];Sources: apps/web/lib/api/network/calculate-partner-ranking.ts:77-82
The scoring model for the discovery tab yields a total score between 0 and 265+ points, synthesized from three additive components:
| Scoring Component | Point Range | Weight / Calculation Mechanics |
|---|---|---|
| Trusted Partner Bonus | 200 points | Applied exclusively when networkStatus = "trusted" to ensure top-tier priority boost. |
| Similarity Score | 0–50 points | Weighted performance across similar programs where similarity score exceeds 0.3. |
| Program Match Score | 0–15 points | 2 points per similar program the partner is enrolled in (capped at 15 points). |
* Scoring Breakdown (0-65+ points):
*
* 1. Trusted Partner Bonus (200 points): Top priority boost
* - Partners with networkStatus = "trusted" get 200 bonus points
* - This ensures trusted partners appear at the very top
*
* 2. Similarity Score (0-50 points): Performance in similar programs
* - Sums weighted performance across similar programs (similarityScore > 0.3)
* - Each program's performance weighted by its similarity score
* - Partners with more similar programs score higher (capped at 50)
* - Includes: consistency (20%), conversion rate (10%), LTV (15%), commissions (5%)
*
* 3. Program Match Score (0-15 points): Count of similar programs
* - Rewards partners enrolled in many similar programs
* - 2 points per similar program (capped at 15)
*
* Final Score = Trusted Bonus + Similarity + Match (0-265+ points)
*Sources: apps/web/lib/api/network/calculate-partner-ranking.ts:18-35
Note
The similarity score sums individual program performance metrics using specific internal weights: consistency at 20%, conversion rate at 10%, LTV at 15%, and commissions at 5%.
Sources: apps/web/lib/api/network/calculate-partner-ranking.ts:28-29
Sorting and Shared Platform Resolution
The sorting mechanism is handled by buildOrderByClause, which accepts starred, sortBy, and platform arguments. If starred is true, results are ordered descending by dp.starredAt. If sorting by subscribers with an active platform filter, the query extracts the maximum verified subscriber count for that specific platform type before ordering.
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
Administrators can inspect overlapping partners across the network via the shared platforms route. Verified platforms are filtered, and website identifiers are stripped of their www prefix using getDomainWithoutWWW to evaluate domain-level matches rather than exact URL strings.
const verifiedPlatforms = partner.platforms.filter(
(platform) => platform.verifiedAt !== null,
);
// a partner has at most one platform per type, so there is at most one website
const websitePlatform = verifiedPlatforms.find(
(platform) => platform.type === "website",
);
const websiteDomain = websitePlatform
? getDomainWithoutWWW(websitePlatform.identifier) ?? null
: null;
const matchConditions: Prisma.PartnerPlatformWhereInput[] = [];
for (const platform of verifiedPlatforms) {
if (platform.type === "website") {
// website identifiers are full URLs with inconsistent normalization,
// so match on the domain instead of the exact identifier
if (websiteDomain) {
matchConditions.push({
identifier: {
contains: websiteDomain,
},
type: platform.type,
});
}Sources: apps/web/app/ee/api/admin/partners/partnerId/shared-platforms/route.ts:23-49
Warning
Website matches utilizing contains queries are post-processed to verify exact domain equality (getDomainWithoutWWW(match.identifier) === websiteDomain), preventing partial substring collisions such as matching example.com against notexample.com.
Sources: apps/web/app/ee/api/admin/partners/partnerId/shared-platforms/route.ts:95-103
Marketplace UI and Navigation Surfaces
Public Directory Layout and Navigation Surfaces
The program marketplace features multiple navigation entry points and directory layouts. In public navigation menus, ProgramMarketplaceSection renders a navigation link pointing to /marketplace with dynamic UTM tracking parameters (NAV_UTM_PARAMS) where utm_content is explicitly set to "Program Marketplace". The navigation item incorporates a decorative floating grid pattern via Grid with a cell size of 60 and pattern offsets of [-12, -32]. It also features animated company logo nodes floating via motion transitions.
export function ProgramMarketplaceSection({ domain }: { domain: string }) {
return (
<NavigationMenuLink asChild>
<Link
href={createHref("/marketplace", domain, {
...NAV_UTM_PARAMS,
utm_campaign: domain,
utm_content: "Program Marketplace",
})}
className={cn(
"group relative isolate flex items-center overflow-hidden bg-neutral-50 px-6 py-3 transition-colors duration-75",
"hover:bg-neutral-100 dark:border-white/[0.15] dark:bg-white/[0.03] dark:hover:bg-white/[0.06]",
)}
>
{/* offset so a horizontal cell edge sits just below the middle of the bar */}
<Grid
cellSize={60}
patternOffset={[-12, -32]}
className="text-neutral-200/60 [mask-image:linear-gradient(90deg,transparent_20%,black)] dark:text-white/10"
/>Sources: packages/ui/src/nav/content/program-marketplace.tsx:100-119
Note
The floating logo canvas (ProgramMarketplaceLogos) uses deterministic drift physics based on slot indices, calculating movement amplitudes (amplitude = 0.6 + depth * 0.8) with Math.sin and Math.cos to simulate water buoyancy and parallax depth across 16 predefined company logo slots.
Sources: packages/ui/src/nav/content/program-marketplace.tsx:9-50
External routing is structured around dynamic segments at /marketplace/[[...segments]]. Metadata generation (generateMetadata) parses segment lengths to construct context-aware SEO titles, descriptions, and OpenGraph images. Specifically, category paths matching /marketplace/c/[category] resolve human-readable labels from PROGRAM_CATEGORIES_MAP and generate category-specific OpenGraph image endpoints.
export async function generateMetadata(props: {
params: Promise<{ segments?: string[] }>;
}): Promise<Metadata> {
const { segments = [] } = await props.params;
const pathname = `/marketplace${segments.length > 0 ? `/${segments.join("/")}` : ""}`;
const currentYear = new Date().getFullYear();
let title = `Best SaaS affiliate programs in ${currentYear}`;
let description = `Browse and apply to the best SaaS affiliate programs on Dub's Partner Network.`;
let image = "https://assets.dub.co/og/marketplace.jpg";
if (segments.length === 1 && segments[0] === "all") {
title = `Top SaaS affiliate programs in ${currentYear}`;
} else if (
segments.length === 1 &&
segments[0] !== "all" &&
segments[0] !== "popular"
) {
const program = await getNetworkProgram({ slug: segments[0] });
if (program) {
title = `${program.name} Affiliate Program`;
description =
program.description ||
`Join the ${program.name} affiliate program on Dub's Program Marketplace.`;
image =
program.marketplaceHeaderImage ||
`${APP_DOMAIN}/api/og/program?slug=${program.slug}`;
}
} else if (segments.length === 2 && segments[0] === "c") {
const category = Object.values(Category).find(
(value) => value.toLowerCase() === segments[1],
);
if (category) {
// categoryMeta should always return a value, but just in case
const categoryMeta = PROGRAM_CATEGORIES_MAP[category];
const label = categoryMeta?.label ?? category.replaceAll("_", " ");
title = `Best ${label} SaaS Affiliate Programs in ${currentYear}`;
description =
categoryMeta?.listPageDescription ??
`Browse the best ${label} affiliate programs on Dub's Program Marketplace.`;
image = `${APP_DOMAIN}/api/og/program/categories?categorySlug=${category.toLowerCase()}`;
}
}Sources: apps/web/app/app.dub.co/marketplace/...segments/page.tsx:14-59
Category Navigation and Promo Banners
The marketplace home view (MarketplaceHomePage) aggregates summary statistics via /api/network/programs/summary using SWR, rendering featured programs, category sections via MarketplaceCategoriesSection, and organized rows using MarketplaceProgramRow driven by MARKETPLACE_HOME_ROWS.
export function MarketplaceHomePage() {
const { data: summary } = useSWR<MarketplaceProgramsSummary>(
"/api/network/programs/summary",
fetcher,
{ revalidateOnFocus: false },
);
return (
<div className="flex flex-col gap-10">
{summary ? (
<FeaturedPrograms programs={summary.featuredPrograms} />
) : (
<FeaturedProgramsSkeleton />
)}
<MarketplaceCategoriesSection />
{MARKETPLACE_HOME_ROWS.map((row) => (
<MarketplaceProgramRow
key={row.key}
title={row.title}
viewAllHref={row.viewAllHref}
showViewAllCard={row.showViewAllCard}
programs={
summary
? summary.categories[row.key] ?? summary[row.key]
: undefined
}
/>
))}
</div>Sources: apps/web/ui/program-marketplace/pages/marketplace-home-page.tsx:19-47
For partner dashboards, promotional visibility is handled by ProgramMarketplaceBanner and ProgramMarketplaceCard, which check partner profile state and promo status. The banner supports animated expansion and collapse states, offering a direct call-to-action to view the marketplace alongside a dismiss button that downgrades the promo state to a card variant.
export function ProgramMarketplaceBanner() {
const { partner } = usePartnerProfile();
const { status, setStatus } = useProgramMarketplacePromo();
return (
<AnimatePresence>
{partner && status === "banner" && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>Sources: apps/web/ui/program-marketplace/program-marketplace-banner.tsx:12-25
The internal programs list page (MarketplaceProgramsListPage) manages pagination and active filters by querying /api/network/programs with query parameters derived from the active category pathname. Pagination uses PROGRAM_NETWORK_MAX_PAGE_SIZE and renders PaginationControls inside a sticky bottom bar when program counts exceed single-page limits.
export function MarketplaceProgramsListPage() {
const pathname = usePathname();
const { getQueryString, searchParamsObj } = useRouterStuff();
const categoryParam = getMarketplaceCategoryFromPathname(pathname);
const queryString = getQueryString(
categoryParam ? { category: categoryParam } : undefined,
);
const { data: programsCount, error: countError } = useNetworkProgramsCount({
query: categoryParam ? { category: categoryParam } : undefined,
});
const {
data: programs,
error,
isValidating,
} = useSWR<NetworkProgramProps[]>(
`/api/network/programs${queryString}`,
fetcher,
{ revalidateOnFocus: false, keepPreviousData: true },
);
const { pagination, setPagination } = usePagination(
PROGRAM_NETWORK_MAX_PAGE_SIZE,
);
const { activeFilters, onRemoveAll } = useProgramNetworkFilters();
const hasActiveFilters =
activeFilters.length > 0 || Boolean(searchParamsObj.search);
return (
<div className="flex flex-col gap-4">
<MarketplaceListToolbar variant="internal" />
{error || countError ? (
<div className="text-content-subtle py-12 text-sm">
Failed to load programs
</div>
) : !programs || programs?.length ? (
<div>
<div className="min-h-[300px]">
{programs ? (
<MarketplaceProgramGrid
programs={programs}
className={cn(isValidating && "opacity-50")}
/>
) : (
<MarketplaceProgramGridSkeleton />
)}
</div>
<div className="sticky bottom-0 mt-4 rounded-b-[inherit] border-t border-neutral-200 bg-white px-3.5 py-2">
<PaginationControls
pagination={pagination}
setPagination={setPagination}
totalCount={programsCount || 0}
unit={(p) => `program${p ? "s" : ""}`}
/>
</div>Sources: apps/web/ui/program-marketplace/pages/marketplace-programs-list-page.tsx:19-79
Card Visualizations and Accent Color Extraction
Program representations utilize specialized card components including FeaturedProgramCard, MarketplaceProgramCard, and MarketplaceProgramHero. Each card dynamically computes aesthetic backdrops using image accent color extraction hooks (useImageAccentColor). If color extraction is pending or blocked, cards fall back to deterministic palette arrays.
| Card Component | Layout Structure | Primary Visual Elements |
|---|---|---|
| FeaturedProgramCard | Responsive flex layout with header banner integration (sm:min-h-[340px]) | Program logo, status badge, description, rewards summary, and external website link with globe icon. |
| MarketplaceProgramCard | Vertical grid cell card with hover drop shadow (hover:drop-shadow-card-hover) | Circular avatar, program name, truncated 2-line description, and structured reward badges. |
| MarketplaceProgramHero | Extended full-width hero header with banner image slot (min-h-[640px] when banner active) | Large avatar, title, category tags via ProgramCategory, website link, and action slot container. |
export function FeaturedProgramCard({
program,
externalMarketplace = false,
colorIndex = 0,
}: {
program: NetworkProgramProps;
externalMarketplace?: boolean;
colorIndex?: number;
}) {
const router = useRouter();
const rewards = program.rewards;
// Derive the tint from the program's image
// Fall back to the palette if extraction is blocked
const { color: accentColor, ready: accentReady } = useImageAccentColor(
program.marketplaceHeaderImage ?? program.logo,
);
const backgroundColor = accentColor ?? getFeaturedCardBackground(colorIndex);
return (
<article className="relative flex h-full flex-col-reverse gap-4 overflow-hidden rounded-2xl border border-neutral-100 p-2 sm:min-h-[340px] sm:flex-row">Sources: apps/web/ui/program-marketplace/featured-program-card.tsx:35-55
Important
FeaturedProgramCard cycles through an array of 10 pastel background hex codes (FEATURED_CARD_BACKGROUNDS) using modulo arithmetic (colorIndex % length), guaranteeing consistent chromatic distribution across items when image accent extraction is not yet ready.
Sources: apps/web/ui/program-marketplace/featured-program-card.tsx:14-33
Application Analytics and Conversion Tracking
Visitor Device Context and URL Path Routing
Tracking marketplace visitor interactions and application clicks involves capturing client device contexts and resolving incoming request URLs into network program slugs. The tracking subsystem parses user-agent headers to record operating system details, browser context, architecture, and referral origins. When an external program page initializes via MarketplaceExternalProgramPage, it mounts an ApplicationAnalytics component alongside the external shell wrapper and apply button.
return (
<>
<ApplicationAnalytics />
<MarketplaceExternalShell variant="none">
<MarketplaceProgramDetailsLayoutSources: apps/web/ui/program-marketplace/external/marketplace-external-program-page.tsx:26-30
Note
If getNetworkProgram fails to resolve a valid program record for a given programSlug, MarketplaceExternalProgramPage immediately triggers a server-side redirect to /marketplace.
Sources: apps/web/ui/program-marketplace/external/marketplace-external-program-page.tsx:18-24
Program Slug Identification Routing
The route tracking logic uses identityProgramSlug to map incoming request URLs to specific program slugs or marketplace directory partitions. The parser evaluates pathname segments against known route patterns to determine whether a request targets an individual program, a category view, or platform registration.
| Route Pattern | Parsed Path Structure | Output Slug and Marketplace Flag |
|---|---|---|
| Root Marketplace | /marketplace | programSlug: null, isMarketplace: true |
| Category View | /marketplace/c/{category} | programSlug: null, isMarketplace: true |
| Reserved Slug Check | /marketplace/{reservedSlug} | programSlug: null, isMarketplace: true |
| Direct Marketplace Program | /marketplace/{programSlug} | programSlug: parts[1].toLowerCase(), isMarketplace: true |
| Legacy Marketplace Program | /programs/marketplace/{programSlug} | programSlug: parts[2].toLowerCase(), isMarketplace: true |
| Standard Program Path | /programs/{programSlug} | programSlug: parts[1].toLowerCase(), isMarketplace: false |
| Platform Registration | /register | programSlug: NETWORK_PROGRAM_SLUG, isMarketplace: false |
| Short Slug Route | /{programSlug} | programSlug: parts[0].toLowerCase(), isMarketplace: false |
function identityProgramSlug(url: string) {
try {
const urlObj = new URL(url);
const parts = urlObj.pathname.split("/").filter(Boolean);
if (parts.length === 0) {
return { programSlug: null, isMarketplace: false };
}
if (parts[0] === "register") {
return { programSlug: NETWORK_PROGRAM_SLUG, isMarketplace: false };
}
if (parts[0] === "marketplace") {
if (parts.length === 1) {
return { programSlug: null, isMarketplace: true };
}
if (parts[1] === "c") {
return { programSlug: null, isMarketplace: true };
}
if (parts.length === 2) {
if (MARKETPLACE_RESERVED_SLUGS.has(parts[1])) {
return { programSlug: null, isMarketplace: true };
}
return {
programSlug: parts[1].toLowerCase(),
isMarketplace: true,
};
}
return { programSlug: null, isMarketplace: true };
}
if (parts[0] === "programs" && parts[1] === "marketplace") {
if (!parts[2] || MARKETPLACE_RESERVED_SLUGS.has(parts[2])) {
return { programSlug: null, isMarketplace: false };
}
return {
programSlug: parts[2].toLowerCase(),
isMarketplace: true,
};
}
if (parts[0] === "programs") {
if (!parts[1]) {
return { programSlug: null, isMarketplace: false };
}
return {
programSlug: parts[1].toLowerCase(),
isMarketplace: false,
};
}
return {
programSlug: parts[0].toLowerCase(),
isMarketplace: false,
};Sources: apps/web/app/ee/api/track/application/route.ts:419-480
Caution
The identityProgramSlug function wraps its URL parsing inside a try...catch block; malformed or unparseable URL strings do not throw errors but instead silently return a fallback object with programSlug: null and isMarketplace: false.
Sources: apps/web/app/ee/api/track/application/route.ts:481-484
Marketplace Administration and Metadata Operations
Admin Dashboard and Program Sorting
Administrative management of marketplace listings is handled through designated administrative dashboard views where operators can update program descriptions, manage category classifications, and alter display rankings. The administrative interface manages ranking positions by tracking original and modified index states, computing updates when programs are reordered, and falling back to MAX_MARKETPLACE_RANKING (2147483647) for unranked items.
const MAX_MARKETPLACE_RANKING = 2147483647;
type MarketplaceProgram = {
id: string;
name: string;
slug: string;
logo: string | null;
url: string | null;
description: string | null;
addedToMarketplaceAt: string | Date;
marketplaceRanking: number;
categories: Category[];
};
Sources: apps/web/app/ee/admin.dub.co/dashboard/programs/page.tsx:25-38
Warning
Programs assigned MAX_MARKETPLACE_RANKING are excluded from standard positional sequencing calculations, ensuring unranked entries do not disrupt custom ordering among featured marketplace listings.
Sources: apps/web/app/ee/admin.dub.co/dashboard/programs/page.tsx:97-138
Operators can also review recent programs via a dedicated administration view that filters workspaces by plan tier and exposes direct actions to add unlisted programs into the public marketplace directory.
const filters = useMemo(
() => [
{
key: "plan",
icon: CreditCard,
label: "Plan",
singleSelect: true,
options: plans.map((value) => ({
value,
label: capitalize(value) ?? value,
})),
},
],
[],
);Sources: apps/web/app/ee/admin.dub.co/dashboard/programs/recent/page.tsx:59-73
Sitemap Generation and Category Metadata
Search engine optimization and metadata generation leverage automated sitemap compilation routines alongside dynamic Open Graph image generators. The sitemap generation routine inspects incoming request host headers to determine domain context and construct corresponding URL entries.
| Host Context | Query Target | Generated Sitemap Paths |
|---|---|---|
Partners Hostname (PARTNERS_HOSTNAMES) | prisma.program.findMany with published default groups | https://partners.dub.co/{program.slug} |
Application Hostname (isAppHostname) | prisma.program.findMany where addedToMarketplaceAt is not null | https://dub.co/marketplace, https://dub.co/marketplace/programs, category paths, and individual program URLs |
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(),
}));
}Sources: apps/web/app/sitemap.ts:20-44
Important
When executing on application hostnames, the sitemap generator iterates over all values of the Prisma Category enum to construct category-specific directory links alongside individual marketplace program records.
Sources: apps/web/app/sitemap.ts:78-86
Category-specific Open Graph images are dynamically rendered using Next.js image response utilities. The category OG route extracts the categorySlug search parameter, matches it against PROGRAM_CATEGORIES, and renders a styled SVG grid layout with Inter typography and category metadata.
export async function GET(req: NextRequest) {
const categorySlug = req.nextUrl.searchParams.get("categorySlug");
if (!categorySlug) {
return new Response("Missing 'categorySlug' parameter", {
status: 400,
});
}
const category = PROGRAM_CATEGORIES.find(
({ id }) => id.toLowerCase() === categorySlug.toLowerCase(),
);
if (!category) {
return new Response("Category not found", {
status: 404,
});
}Sources: apps/web/app/api/og/program/categories/route.tsx:15-32
Onboarding Integration
Workspace onboarding connects new affiliate programs to the application routing flow by validating active user sessions and project domain bindings. The onboarding step retrieves domain records stored in Redis or workspace configurations, redirecting unconfigured users to domain setup before rendering the client onboarding page.
const data = await redis.get<{ domain: string; userId: string }>(
`onboarding-domain:${workspace.id}`,
);
const onboardingDomain =
data && data.domain && data.userId === user.id ? data.domain : null;
const domain = onboardingDomain || workspace.domains[0]?.slug;
if (!domain)
redirect(`/onboarding/domain?workspace=${slug}&product=partners`);
Sources: apps/web/app/app.dub.co/onboarding/onboarding/steps/program/page.tsx:38-49
Workspaces lacking configured programs display an empty state component featuring partner preview cells that utilize repeating coordinate mappings over a shared asset sprite.
const EXAMPLE_PARTNERS = [
{
name: "Lauren Anderson",
country: "US",
revenue: 1_800,
payouts: 550,
index: 0,
},
{
name: "Elias Weber",
country: "DE",
revenue: 783,
payouts: 235,
index: 4,
},
{
name: "Hiroshi Tanaka",
country: "JP",
revenue: 19_200,
payouts: 5_700,
index: 3,
},
{
name: "Mia Taylor",
country: "US",
revenue: 22_600,
payouts: 6_800,
index: 1,
},
];
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/program-empty-state.tsx:60-90
Related
On This Page
OverviewPublic Marketplace Data FetchingServer Fetchers and Prisma QueriesPublic API Routes and Edge CachingProgram CategoriesProgram Similarity Computation EngineScheduled Execution and Batching StrategyMulti-Dimensional Similarity MetricsComponent Algorithm ImplementationPartner Ranking and Discovery ScoringDiscoverability and Ranking AlgorithmSorting and Shared Platform ResolutionMarketplace UI and Navigation SurfacesPublic Directory Layout and Navigation SurfacesCategory Navigation and Promo BannersCard Visualizations and Accent Color ExtractionApplication Analytics and Conversion TrackingVisitor Device Context and URL Path RoutingProgram Slug Identification RoutingMarketplace Administration and Metadata OperationsAdmin Dashboard and Program SortingSitemap Generation and Category MetadataOnboarding IntegrationRelated