Getting Started
Core Architecture
Link Engine
Analytics & Attribution
Partners & Affiliates
Third-Party Integrations
Identity & Security
Automation & Messaging
Developer Tools
The following files were used as context for generating this wiki page:
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, apps/web/lib/api/network/calculate-partner-ranking.ts:1-39
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, apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:71-122
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, apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:1-24
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, apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:52-113, apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:168-214
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).
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-135, apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-category-similarity.ts:4-43, apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts:10-44
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, apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts:39-42
The ProgramSimilarity model relies on a composite unique constraint and indexes to maintain fast lookup performance for similarity recommendations.
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/app/ee/api/cron/network/calculate-program-similarities/route.ts:25, apps/web/app/ee/api/cron/network/calculate-program-similarities/route.ts:147-165, apps/web/app/ee/api/cron/network/calculate-program-similarities/calculate-partner-similarity.ts:14-25, apps/web/prisma/schema/network.prisma:25-40
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, apps/web/lib/api/network/calculate-partner-ranking.ts:14-29
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:
withWorkspace() — Validates workspace authentication and extracts workspace context along with request search parameters.getDefaultProgramIdOrThrow() — Resolves the default program identifier for the current workspace.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.getNetworkPartnersQuerySchema.parse() — Validates query parameters including partnerIds, status, page, pageSize, country, starred, sortBy, and platform.status is not "discover": executes partnerNetworkListingWhere(), queries prisma.discoveredPartner.findMany(), and maps results through NetworkPartnerSchema.parse().status is "discover": invokes calculatePartnerRanking() passing the structured parameters and similar programs list, then parses and formats the returned ranking array.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.
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.
The sorting order of discovered partners is determined by buildOrderByClause(), which handles pinned or starred partners, platform subscriber counts, and relevance ranking.
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`;
}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.
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, apps/web/app/ee/api/admin/partners/partnerId/shared-platforms/route.ts:97-103
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.
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.
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:39-52, apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:67-81
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.
Within the discover tab, operators can filter potential partners by their connected social platforms or website presence using a toggle group component.
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:54-65, apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:224-259
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:
useEffect() inspects the URL search parameters for a partnerId. If present, it updates detailsSheetState to open the sheet.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.useMemo() calculates previousPartnerId and nextPartnerId by finding the index of the current partner within the loaded list.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, apps/web/app/app.dub.co/dashboard/slug/ee/program/network/page-client.tsx:393-425
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.
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, apps/web/ui/program-marketplace/program-marketplace-card.tsx:12-20, apps/web/ui/program-marketplace/program-marketplace-banner.tsx:12-18
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:
generateMetadata() reads params to extract the segments array, defaulting to an empty list for the marketplace home root.segments.length === 1 and equals "all", it sets the title to top SaaS affiliate programs for the current year.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.segments.length === 2 and segments[0] === "c", it scans Category enums to match segments[1], resolving category-specific labels and API OpenGraph endpoints.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
Programs are rendered across discovery grids and detail views using specialized components such as MarketplaceProgramHero, FeaturedProgramCard, and MarketplaceProgramsListPage.
Sources: apps/web/ui/program-marketplace/marketplace-program-hero.tsx:13-21, apps/web/ui/program-marketplace/featured-program-card.tsx:35-43, apps/web/ui/program-marketplace/pages/marketplace-programs-list-page.tsx:19-42
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, apps/web/ui/program-marketplace/marketplace-program-hero.tsx:23-26
The sitemap generator in apps/web/app/sitemap.ts inspects incoming request headers to determine host environments and construct matching XML sitemap entries.
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
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, apps/web/app/ee/partners.dub.co/auth-other/invite/page.tsx:18-42, apps/web/lib/network/get-program-network-invite-email-defaults.ts:13-27
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.
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;
}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.
The partner invite acceptance page (AcceptPartnerInvitePage) manages user onboarding onto partner profiles through client-side state handling and SWR cache mutations.
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.
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.
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.
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, apps/web/ui/program-marketplace/marketplace-program-header-controls.tsx:49-189
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.
Sources: apps/web/app/ee/partners.dub.co/dashboard/profile/network-approval-guide.tsx:22-38, apps/web/app/ee/partners.dub.co/dashboard/profile/network-approval-guide.tsx:110-127
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.
The MarketplaceProgramHeaderControls component manages program enrollment actions, toggling between invite acceptance, dashboard redirection, and application sheet triggers based on current enrollment status.
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.
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.
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
}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.