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:
Partner Program Management provides a comprehensive engine for orchestrating affiliate and referral ecosystems within workspaces, empowering organizations to scale product-led growth through structured partner participation. It addresses the complexity of multi-tenant tracking, partner recruitment, custom segmentation, and tiered reward structures by bridging workspace administration with public application portals and automated workflows. Core design decisions prioritize modular group architectures, granular link attribution with reward overrides, and secure submission pipelines equipped with fraud detection capabilities. The system integrates closely with workspace authentication guards, plan entitlement checks, navigation sidebars, and specialized subdomain middleware to route partners and administrators across dedicated portal environments seamlessly. Sources: apps/web/lib/actions/partners/create-program.ts:34-241, apps/web/ui/layout/sidebar/app-sidebar-nav.tsx:90-124, apps/web/lib/middleware/partners.ts:25-122
Workspace program initialization gates access to partner features via plan entitlement validation, explicit user session permission checks, and programmatic onboarding creation steps. Before a partner program can be initialized or administered, the workspace must satisfy specific plan capabilities and possess an active onboarding store state.
Sources: apps/web/lib/actions/partners/create-program.ts:34-74, apps/web/app/app.dub.co/dashboard/slug/ee/program/auth.tsx:1-60
The authorization boundary for partner programs relies on evaluating the workspace plan capabilities and checking user-level access permissions. The ProgramAuth component and createProgram action inspect plan parameters using getPlanCapabilities and isLegacyBusinessPlan to verify whether a workspace is permitted to manage a partner program.
const { canManageProgram, canMessagePartners } = getPlanCapabilities(
workspace.plan,
);
if (
!canManageProgram ||
isLegacyBusinessPlan({
plan: workspace.plan,
partnersLimit: workspace.partnersLimit,
})
) {
throw new Error(
"Your current plan does not have access to create a partner program. Please upgrade to a higher plan to proceed.",
);
}Sources: apps/web/lib/actions/partners/create-program.ts:55-69, apps/web/app/app.dub.co/dashboard/slug/ee/program/auth.tsx:37-57
Warning
Workspaces flagged under a legacy business plan configuration or lacking program management capabilities will trigger an immediate exception during creation or render the ProgramEmptyState view, blocking access to partner dashboards.
Sources: apps/web/lib/actions/partners/create-program.ts:59-69, apps/web/app/app.dub.co/dashboard/slug/ee/program/auth.tsx:47-57
When a user submits program onboarding data, the createProgram action executes a strict sequence of operations inside a transactional database boundary. It validates the domain, processes optional logo storage uploads, provisions folders, creates the program record, seeds default groups, and updates workspace settings.
The execution steps follow a precise transactional ordering:
programDataSchema.parse(store.programOnboarding) extracts and validates parameters including name, domain, URL, reward type, and amounts.getDomainOrThrow({ workspace, domain }) validates that the workspace owns or can use the target domain.storage.upload uploads an optional program logo using a generated storage key prefixed with programs/{programId}/.prisma.$transaction wraps folder creation, program instantiation, default group seeding, and project metadata updates.Tip
If a workspace lacks an invoicePrefix during program creation, the initialization sequence automatically generates an 8-character random string to ensure billing record consistency.
Partner groups segment participants by rewards, discounts, performance metrics, location, and customized criteria within a program. When requests are made for partner lander pages, application forms, or link generation, the system resolves specific group slugs—defaulting to DEFAULT_PARTNER_GROUP.slug if none is explicitly specified.
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/groups/page.tsx:9-14, apps/web/app/ee/partners.dub.co/dashboard/programs/programSlug/apply/page.tsx:22-25, apps/web/app/ee/partners.dub.co/apply/programSlug/default/apply/page.tsx:18-24, apps/web/app/ee/partners.dub.co/apply/programSlug/default/page.tsx:18-23
When generating tracking links for partners via the API route, the system executes a precise retrieval and application sequence to assign default URLs, tracking properties, UTM parameters, and reward overrides.
The link creation process follows these sequential execution steps:
createPartnerLinkSchemaInternal.parse validates incoming payload parameters including partnerId, tenantId, url, key, linkProps, and reward IDs.getProgramOrThrow verifies that the program exists and possesses a configured domain and url.prisma.programEnrollment.findUnique queries the database for the partner, extracting their associated partnerGroup, partnerGroupDefaultLinks, and utmTemplate.processLink constructs the base link payload with target URLs falling back to partnerGroup.partnerGroupDefaultLinks[0].url if omitted.applyGroupUtmToLink injects group-level UTM templates and partner identifiers into the link configuration.throwIfInvalidRewards validates any specified link-level reward assignments against the target partner group.createLink persists the fully configured partner link.Caution
If a partner is not associated with a valid partner group (!partnerGroup), the link creation request immediately aborts with a not_found API error preventing orphan records.
Partner programs expose public landing pages and application forms resolved dynamically by programSlug and optional groupSlug. When visitors hit ApplyPage, the system fetches the program and group metadata using getProgram. If the lander data or application form data is unconfigured or unpublished, requests for the default group either redirect to the marketplace program page or return a 404 error.
Sources: apps/web/app/ee/partners.dub.co/apply/programSlug/default/apply/page.tsx:12-44, apps/web/app/ee/partners.dub.co/apply/programSlug/default/page.tsx:13-43
Sources: apps/web/app/ee/partners.dub.co/apply/programSlug/default/apply/page.tsx:21-44, apps/web/app/ee/partners.dub.co/apply/programSlug/default/page.tsx:20-43
The createProgramApplicationAction server action handles public form submissions and in-app applications. It enforces rate limits, validates payloads, processes existing user sessions, and executes database mutations.
The step-by-step ingestion and execution flow proceeds as follows:
assertRateLimit restricts submissions to 3 requests per minute per program per IP using Upstash policies.prisma.program.findUniqueOrThrow retrieves program relations, group configurations, and workspace webhook settings.getSession identifies whether an authenticated partner session exists.createApplicationAndEnrollment, the system validates profile completeness checklist progress via getNetworkProfileChecklistProgress and ensures network status is approved or trusted.autoRejectPartnerJob is dispatched with a 30-minute delay.createApplication persists the application, records country headers (falling back to "US" in local dev/CI environments), sets a 7-day programApplicationIds HTTP-only cookie, and updates associated programApplicationEvent records.When createApplicationAndEnrollment executes successfully for logged-in partners, asynchronous background tasks run via Vercel's waitUntil helper function to handle notifications, webhooks, fraud detection, and analytics tracking.
Warning
If a logged-in partner attempts an in-app application with an incomplete network profile checklist or a non-approved network status (networkStatus not equal to "approved" or "trusted"), the action immediately throws an error blocking submission.
Caution
Unauthenticated applications set a 7-day persistent HTTP-only cookie named programApplicationIds tracking submitted application IDs. If the corresponding application event cookie is present, it directly updates the programApplicationEvent table row.
The application review and approval pipeline handles workspace dashboard management of candidate program submissions. Workspace administrators use the applications dashboard interface to inspect pending applications, filter records across statuses, and invoke approval mutations that finalize partner enrollments.
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/applications/applications-nav.tsx:8-42, apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/applications/page.tsx:1-5
The dashboard layout is structured around an applications shell with navigation tabs mapped to ProgramApplicationStatus enums. The navigation component preserves search parameters (search, country, groupId, sortBy, sortOrder) across view switches.
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/applications/layout.tsx:1-26, apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/applications/applications-nav.tsx:1-42
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/applications/applications-nav.tsx:16-32
When an administrator approves a candidate partner submission, the request flows through either a server action or a REST API route. The execution sequence guarantees workspace permissions and plan validation before mutating state:
withWorkspace or authActionClient verifies the session context, workspace association, and role permissions via throwIfNoPermission requiring owner or member roles.getDefaultProgramIdOrThrow resolves the target program identifier from the active workspace.approveProgramApplication executes the core enrollment mutation for the given partnerId, programId, groupId, and approving userId.Sources: apps/web/lib/actions/partners/approve-program-application.ts:1-34, apps/web/app/ee/api/program-applications/approve/route.ts:9-32
Note
The API endpoint at /api/program-applications/approve strictly enforces subscription plan entitlements, requiring the workspace to be on the business, advanced, or enterprise plans.
Sources: apps/web/lib/actions/partners/approve-program-application.ts:10-33, apps/web/app/ee/api/program-applications/approve/route.ts:9-31
The partner lifecycle and link management subsystem controls enrolled partners, directory queries, status mutations, partner switching, and tracking link generation with custom reward overrides. Workspace operators interact with enrolled partners via the partners directory view and individual partner management layouts.
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/page.tsx:1-28, apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/partnerId/layout.tsx:1-71
The REST API endpoint at GET /api/partners retrieves all enrolled partners for a program, supporting custom filters and sorting parameters.
export const GET = withWorkspace(
async ({ workspace, searchParams }) => {
const programId = getDefaultProgramIdOrThrow(workspace);
const filterOverrides = parsePartnerFilterParams(searchParams);
const paramsToParse = {
...searchParams,
...(filterOverrides.partnerTagId && {
partnerTagId: filterOverrides.partnerTagId,
}),
...(filterOverrides.groupId !== undefined && {
groupId: filterOverrides.groupId,
}),
...(filterOverrides.country !== undefined && {
country: filterOverrides.country,
}),
};
const { sortBy: sortByWithOldFields, ...parsedParams } =
getPartnersRouteQuerySchema.parse(paramsToParse);
const sortBy =
{
clicks: "totalClicks",
leads: "totalLeads",
conversions: "totalConversions",
sales: "totalSaleAmount",
saleAmount: "totalSaleAmount",
totalSales: "totalSaleAmount",
}[sortByWithOldFields] || sortByWithOldFields;
const partners = await getPartners({
...parsedParams,
sortBy,
programId,
});
// ...
},
{
requiredPlan: ["business", "advanced", "enterprise"],
},
);Legacy sorting fields such as clicks, leads, conversions, sales, saleAmount, and totalSales are mapped directly to canonical column names like totalClicks, totalLeads, totalConversions, and totalSaleAmount.
Partners can have custom tracking links created through POST /api/partners/links. This endpoint validates partner enrollment, checks advanced plan capabilities for reward assignments, and persists link-level rewards.
export const POST = withWorkspace(
async ({ workspace, req, session }) => {
const programId = getDefaultProgramIdOrThrow(workspace);
const {
partnerId,
tenantId,
url,
key,
linkProps,
clickRewardId,
leadRewardId,
saleRewardId,
discountId,
} = createPartnerLinkSchemaInternal.parse(await parseRequestBody(req));
const program = await getProgramOrThrow({
workspaceId: workspace.id,
programId,
});
if (!program.domain || !program.url) {
throw new DubApiError({
code: "bad_request",
message:
"You need to set a domain and url for this program before creating a link.",
});
}
// ...
},
{
requiredPlan: ["business", "advanced", "enterprise"],
requiredRoles: ["owner", "member"],
},
);Warning
Assigning link-level rewards requires the workspace plan capability canUseAdvancedRewardLogic. Attempting to assign custom link rewards on unauthorized plans throws a forbidden error with the PARTNER_LEVEL_REWARDS_PLAN_ERROR constant.
The dashboard layout for individual partners ([partnerId]/layout.tsx) handles route validation, active partner switching, and modal triggers for administrative actions.
export default function ProgramPartnerLayout({
children,
}: {
children: ReactNode;
}) {
const { slug: workspaceSlug } = useWorkspace();
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const params = useParams() as { slug: string; partnerId: string };
const { partner, error: partnerError } = usePartner({
partnerId: params.partnerId,
});
if (partnerError && partnerError.status === 404) {
redirect(`/${workspaceSlug}/program/partners`);
}
const switchToPartner = (newPartnerId: string) => {
if (params.partnerId === newPartnerId) return;
const url = `${pathname.replace(`/partners/${params.partnerId}`, `/partners/${newPartnerId}`)}?${searchParams.toString()}`;
router.push(url);
};
// ...
}The partner program navigation is integrated into workspace and partner portal layouts via sidebar navigation components. In workspace contexts, NAV_GROUPS defines whether the "Partner Program" or "Short Links" group appears first based on defaultProduct. The partner program navigation item points to /${slug}/program and uses the ConnectedDots4 icon, rendering an active state whenever the pathname starts with the workspace slug and is outside links or settings paths.
In portal contexts, partners-sidebar-nav.tsx establishes top-level navigation groups including Programs, Payouts, Profile settings, and Messages. The Programs group remains active across /programs and /marketplace paths, while the Messages badge dynamically displays unread message counts capped at 9 via unreadMessagesCount.
Program switching within the partner portal is driven by the PartnerProgramDropdown component, which queries partner profiles and program enrollments via SWR hooks. It extracts the current programSlug from route parameters and matches it against approved enrollments to derive the selectedProgram.
const selectedProgram = useMemo(() => {
const program = programEnrollments?.find(
(programEnrollment) => programEnrollment.program.slug === programSlug,
);
return programSlug && program
? {
...program.program,
logo:
program.program.logo || `${OG_AVATAR_URL}${program.program.name}`,
status: program.status,
}
: undefined;
}, [programSlug, programEnrollments]);The dropdown features a command search menu (cmd-k) allowing partners to filter through approved programs. Selecting an alternative program executes a router push using a dynamic href helper that preserves query parameters while replacing the existing program slug in the pathname.
const href = useCallback(
(slug: string) =>
selectedProgram
? `${pathname.replace(selectedProgram.slug, slug)}${searchParamsString.length > 0 ? `?${searchParamsString}` : ""}`
: `/programs/${slug}`,
[pathname, selectedProgram],
);Incoming requests to partner portal subdomains are intercepted by PartnersMiddleware, which evaluates authentication tokens, path structures, and default partner identifiers.
const AUTHENTICATED_PATHS = [
"/programs",
"/marketplace",
"/onboarding",
"/settings",
"/profile",
"/messages",
"/payouts",
"/account",
"/invite",
"/rewind",
];The middleware executes an explicit validation and routing sequence:
parse(req) extracts the path, full path, and search parameters.getUserViaToken(req) resolves the active session user./login?next=... (or /[programSlug]/login for custom program paths).defaultPartnerId (and is not accessing /onboarding, /account, or an invite link), the middleware redirects them to /onboarding.searchParamsObj.next are processed securely while omitting onboarding routes./ or /pn_*) are rewritten or redirected to /programs, while all other requests are rewritten to the underlying /partners.dub.co subdomain.