---
title: "Partner Program Management"
description: "Partner Program Management provides a comprehensive engine for orchestrating affiliate and referral ecosystems within workspaces, empowering organizations to scale product-led growth through struct..."
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/partner-program-management"
---

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

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

- [apps/web/app/app.dub.co/dashboard/slug/ee/program/groups/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/groups/page.tsx)
- [apps/web/app/ee/partners.dub.co/dashboard/programs/programSlug/apply/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/%5BprogramSlug%5D/apply/page.tsx)
- [apps/web/app/ee/api/partners/links/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/links/route.ts)
- [apps/web/lib/actions/partners/create-program.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program.ts)
- [apps/web/app/ee/admin.dub.co/dashboard/partners/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/page.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/page.tsx)
- [apps/web/app/ee/api/partners/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/route.ts)
- [apps/web/app/ee/admin.dub.co/dashboard/partners/trusted/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/trusted/page.tsx)
- [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/app/ee/partners.dub.co/apply/programSlug/default/apply/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(apply)/%5BprogramSlug%5D/(default)/apply/page.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/partnerId/layout.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/%5BpartnerId%5D/layout.tsx)
- [apps/web/lib/actions/partners/approve-program-application.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/approve-program-application.ts)
- [apps/web/ui/layout/sidebar/app-sidebar-nav.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/layout/sidebar/app-sidebar-nav.tsx)
- [apps/web/lib/middleware/partners.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/partners.ts)
- [apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx)
- [apps/web/app/ee/admin.dub.co/dashboard/partners/network/network-partner-application-sheet.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/network-partner-application-sheet.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/applications/layout.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/applications/layout.tsx)
- [apps/web/ui/layout/sidebar/dub-partners-popup.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/layout/sidebar/dub-partners-popup.tsx)
- [apps/web/app/ee/partners.dub.co/apply/programSlug/default/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(apply)/%5BprogramSlug%5D/(default)/page.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/app/ee/api/program-applications/approve/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/program-applications/approve/route.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/page-client.tsx)
- [apps/web/lib/actions/partners/create-program-application.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program-application.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/program-empty-state.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/program-empty-state.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/auth.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/auth.tsx)
- [apps/web/ui/layout/sidebar/partner-program-dropdown.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/layout/sidebar/partner-program-dropdown.tsx)
- [apps/web/scripts/dev/seed.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/dev/seed.ts)
- [apps/web/app/ee/api/partner-profile/programs/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partner-profile/programs/route.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/applications/applications-nav.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/applications/applications-nav.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/applications/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/applications/page.tsx)
</details>

## Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program.ts#L34-L241), [apps/web/ui/layout/sidebar/app-sidebar-nav.tsx:90-124](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/layout/sidebar/app-sidebar-nav.tsx#L90-L124), [apps/web/lib/middleware/partners.ts:25-122](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/partners.ts#L25-L122)

## Program Initialization and Workspace Access

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program.ts#L34-L74), [apps/web/app/app.dub.co/dashboard/slug/ee/program/auth.tsx:1-60](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/auth.tsx#L1-L60)

### Plan Entitlement and Access Control

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.

```typescript
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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program.ts#L55-L69), [apps/web/app/app.dub.co/dashboard/slug/ee/program/auth.tsx:37-57](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/auth.tsx#L37-L57)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program.ts#L59-L69), [apps/web/app/app.dub.co/dashboard/slug/ee/program/auth.tsx:47-57](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/auth.tsx#L47-L57)

### Onboarding Initialization Call Chain

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.

```mermaid
graph TD
  A[programDataSchema.parse] --> B[getDomainOrThrow]
  B --> C[storage.upload]
  C --> D[prisma.$transaction]
  D --> E[tx.folder.upsert]
  E --> F[tx.program.create]
  F --> G[tx.partnerGroup.upsert]
  G --> H[tx.project.update]
```

Sources: [apps/web/lib/actions/partners/create-program.ts:76-238](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program.ts#L76-L238)

The execution steps follow a precise transactional ordering:
1. `programDataSchema.parse(store.programOnboarding)` extracts and validates parameters including name, domain, URL, reward type, and amounts.
2. `getDomainOrThrow({ workspace, domain })` validates that the workspace owns or can use the target domain.
3. `storage.upload` uploads an optional program logo using a generated storage key prefixed with `programs/{programId}/`.
4. `prisma.$transaction` wraps folder creation, program instantiation, default group seeding, and project metadata updates.

Sources: [apps/web/lib/actions/partners/create-program.ts:76-240](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program.ts#L76-L240)

### Workspace Initialization Parameters

| Parameter | Type / Source | Purpose |
| :--- | :--- | :--- |
| `workspace` | `Pick<Project, "id" \| "slug" \| "logo" \| "name" \| "plan" \| "partnersLimit" \| "trialEndsAt" \| "store" \| "webhookEnabled" \| "invoicePrefix">` | Workspace context supplying id, slug, plan, and limits. |
| `user` | `Pick<User, "id" \| "email">` | Authenticated user initializing the program. |
| `isProgramOnboarding` | `boolean` | Optional flag indicating initial setup flow context. |
| `store.programOnboarding` | `Record<string, any>` | JSON store blob containing form inputs parsed by `programDataSchema`. |

Sources: [apps/web/lib/actions/partners/create-program.ts:34-74](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program.ts#L34-L74)

> [!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.

Sources: [apps/web/lib/actions/partners/create-program.ts:233-236](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program.ts#L233-L236)

## Partner Group Architecture and Attribution

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/groups/page.tsx#L9-L14), [apps/web/app/ee/partners.dub.co/dashboard/programs/programSlug/apply/page.tsx:22-25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/%5BprogramSlug%5D/apply/page.tsx#L22-L25), [apps/web/app/ee/partners.dub.co/apply/programSlug/default/apply/page.tsx:18-24](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(apply)/%5BprogramSlug%5D/(default)/apply/page.tsx#L18-L24), [apps/web/app/ee/partners.dub.co/apply/programSlug/default/page.tsx:18-23](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(apply)/%5BprogramSlug%5D/(default)/page.tsx#L18-L23)

### UTM and Reward Configuration Call Chain

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.

```mermaid
graph TD
  A[createPartnerLinkSchemaInternal.parse] --> B[getProgramOrThrow]
  B --> C[prisma.programEnrollment.findUnique]
  C --> D[processLink]
  D --> E[applyGroupUtmToLink]
  E --> F[pickDefinedRewardIds]
  F --> G[throwIfInvalidRewards]
  G --> H[createLink]
```

Sources: [apps/web/app/ee/api/partners/links/route.ts:98-225](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/links/route.ts#L98-L225)

The link creation process follows these sequential execution steps:
1. `createPartnerLinkSchemaInternal.parse` validates incoming payload parameters including `partnerId`, `tenantId`, `url`, `key`, `linkProps`, and reward IDs.
2. `getProgramOrThrow` verifies that the program exists and possesses a configured `domain` and `url`.
3. `prisma.programEnrollment.findUnique` queries the database for the partner, extracting their associated `partnerGroup`, `partnerGroupDefaultLinks`, and `utmTemplate`.
4. `processLink` constructs the base link payload with target URLs falling back to `partnerGroup.partnerGroupDefaultLinks[0].url` if omitted.
5. `applyGroupUtmToLink` injects group-level UTM templates and partner identifiers into the link configuration.
6. `throwIfInvalidRewards` validates any specified link-level reward assignments against the target partner group.
7. `createLink` persists the fully configured partner link.

Sources: [apps/web/app/ee/api/partners/links/route.ts:98-225](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/links/route.ts#L98-L225)

### API Endpoints and Parameters for Partner Links

| Endpoint Method & Path | Validation Schema | Purpose |
| :--- | :--- | :--- |
| `GET /api/partners/links` | `retrievePartnerLinksSchemaInternal` | Retrieves existing tracking links for an enrolled partner, optionally including reward associations. |
| `POST /api/partners/links` | `createPartnerLinkSchemaInternal` | Generates a new tracking link for a partner, applying group UTM templates, default URLs, and optional reward overrides. |

Sources: [apps/web/app/ee/api/partners/links/route.ts:33-226](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/links/route.ts#L33-L226)

> [!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.

Sources: [apps/web/app/ee/api/partners/links/route.ts:155-161](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/links/route.ts#L155-L161)

## Public Application Form and Submission

### Public Application Form Configuration and Lander Resolution

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(apply)/%5BprogramSlug%5D/(default)/apply/page.tsx#L12-L44), [apps/web/app/ee/partners.dub.co/apply/programSlug/default/page.tsx:13-43](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(apply)/%5BprogramSlug%5D/(default)/page.tsx#L13-L43)

```mermaid
graph TD
  A[Incoming Lander / Apply Request] --> B[getProgram]
  B --> C{Program & Group Published?}
  C -- Yes --> D[Parse Schema via Zod]
  C -- No (Default Group) --> E{Marketplace Active?}
  E -- Yes --> F[Redirect to /marketplace/programSlug]
  E -- No --> G["Throw notFound()"]
  C -- No (Custom Group) --> H[Redirect to Default Group Variant]
  D --> I[Render LanderHero / ApplicationFormHero]
  I --> J[Render Rewards, Bounties, & Blocks]
```

Sources: [apps/web/app/ee/partners.dub.co/apply/programSlug/default/apply/page.tsx:21-44](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(apply)/%5BprogramSlug%5D/(default)/apply/page.tsx#L21-L44), [apps/web/app/ee/partners.dub.co/apply/programSlug/default/page.tsx:20-43](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(apply)/%5BprogramSlug%5D/(default)/page.tsx#L20-L43)

### Application Submission Call Chain and Ingestion

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.

```mermaid
graph TD
  A[createProgramApplicationAction] --> B[assertRateLimit: 3 req/min/IP]
  B --> C[prisma.program.findUniqueOrThrow]
  C --> D{Existing Partner Session?}
  D -- Yes --> E{In-App Application?}
  E -- Yes --> F[Check Checklist Progress & Network Status]
  F --> G[createApplicationAndEnrollment]
  D -- No --> H[createApplication]
  H --> I[programApplicationReminderJob.dispatch: 15m delay]
```

Sources: [apps/web/lib/actions/partners/create-program-application.ts:121-241](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program-application.ts#L121-L241)

The step-by-step ingestion and execution flow proceeds as follows:
1. `assertRateLimit` restricts submissions to 3 requests per minute per program per IP using Upstash policies.
2. `prisma.program.findUniqueOrThrow` retrieves program relations, group configurations, and workspace webhook settings.
3. `getSession` identifies whether an authenticated partner session exists.
4. If an existing partner applies via `createApplicationAndEnrollment`, the system validates profile completeness checklist progress via `getNetworkProfileChecklistProgress` and ensures network status is `approved` or `trusted`.
5. If requirements fail during an external application, `autoRejectPartnerJob` is dispatched with a 30-minute delay.
6. For anonymous visitors, `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.

Sources: [apps/web/lib/actions/partners/create-program-application.ts:127-461](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program-application.ts#L127-L461)

### Fraud Checks and Application Events

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.

Sources: [apps/web/lib/actions/partners/create-program-application.ts:325-386](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program-application.ts#L325-L386)

| Background Task | Trigger / Handler Function | Purpose |
| :--- | :--- | :--- |
| Program Notification | `notifyProgramApplication` | Sends application notifications to program administrators. |
| Auto-Approval | `autoApprovePartnerJob` | Automatically approves the partner enrollment if `autoApprovePartnersEnabledAt` is active on the group. |
| Webhook Dispatch | `sendWorkspaceWebhook` | Fires a `partner.application_submitted` workspace webhook payload validated against `partnerApplicationWebhookSchema`. |
| Fraud Detection | `detectAndRecordFraudApplication` | Evaluates submission context to detect and log fraudulent application behavior. |
| Event Tracking | `markApplicationEventSubmitted` | Marks the application lifecycle event as submitted for the given enrollment. |
| Search Index Sync | `queuePartnerSearchSync` | Queues search indexing updates for the newly created program enrollment ID. |

Sources: [apps/web/lib/actions/partners/create-program-application.ts:334-384](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program-application.ts#L334-L384)

> [!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.

Sources: [apps/web/lib/actions/partners/create-program-application.ts:188-212](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program-application.ts#L188-L212)

> [!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.

Sources: [apps/web/lib/actions/partners/create-program-application.ts:423-452](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/create-program-application.ts#L423-L452)

## Application Review and Approval Pipeline

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/applications/applications-nav.tsx#L8-L42), [apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/applications/page.tsx:1-5](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/applications/page.tsx#L1-L5)

### Dashboard Applications Layout and Navigation

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/applications/layout.tsx#L1-L26), [apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/applications/applications-nav.tsx:1-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/applications/applications-nav.tsx#L1-L42)

| Tab ID / Status | Label | Icon Component | Associated Route |
| :--- | :--- | :--- | :--- |
| `ProgramApplicationStatus.pending` | Pending | `CircleHalfDottedClock` | `/${slug}/program/partners/applications` |
| `ProgramApplicationStatus.approved` | Approved | `CircleCheck` | Derived tab interface |
| `ProgramApplicationStatus.rejected` | Rejected | `UserXmark` | Derived tab interface |

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/applications/applications-nav.tsx:16-32](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/applications/applications-nav.tsx#L16-L32)

### Approval Execution Walkthrough

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:

1. `withWorkspace` or `authActionClient` verifies the session context, workspace association, and role permissions via `throwIfNoPermission` requiring `owner` or `member` roles.
2. `getDefaultProgramIdOrThrow` resolves the target program identifier from the active workspace.
3. `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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/approve-program-application.ts#L1-L34), [apps/web/app/ee/api/program-applications/approve/route.ts:9-32](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/program-applications/approve/route.ts#L9-L32)

> [!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/app/ee/api/program-applications/approve/route.ts:28-31](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/program-applications/approve/route.ts#L28-L31)

### Approval API and Action Interfaces

| Interface / Endpoint | Input Validation Schema | Required Roles | Target Handler Function |
| :--- | :--- | :--- | :--- |
| `approveProgramApplicationAction` (Server Action) | `inputSchema` (`partnerId`, `groupId`, `workspaceId`) | `owner`, `member` | `approveProgramApplication` |
| `POST /api/program-applications/approve` (REST API) | `approveProgramApplicationSchema` (`partnerId`, `groupId`) | `owner`, `member` | `approveProgramApplication` |

Sources: [apps/web/lib/actions/partners/approve-program-application.ts:10-33](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/approve-program-application.ts#L10-L33), [apps/web/app/ee/api/program-applications/approve/route.ts:9-31](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/program-applications/approve/route.ts#L9-L31)

## Partner Lifecycle and Link Management

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/page.tsx#L1-L28), [apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/partnerId/layout.tsx:1-71](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/%5BpartnerId%5D/layout.tsx#L1-L71)

### Enrolled Partner Directory and Queries

The REST API endpoint at `GET /api/partners` retrieves all enrolled partners for a program, supporting custom filters and sorting parameters.

```typescript
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"],
  },
);
```

Sources: [apps/web/app/ee/api/partners/route.ts:36-79](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/route.ts#L36-L79)

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`.

Sources: [apps/web/app/ee/api/partners/route.ts:57-65](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/route.ts#L57-L65)

### Tracking Link Generation and Reward Overrides

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.

```typescript
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"],
  },
);
```

Sources: [apps/web/app/ee/api/partners/links/route.ts:93-122](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/links/route.ts#L93-L122)

> [!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.

Sources: [apps/web/app/ee/api/partners/links/route.ts:204-214](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/links/route.ts#L204-L214)

### Partner Layout and State Switching

The dashboard layout for individual partners (`[partnerId]/layout.tsx`) handles route validation, active partner switching, and modal triggers for administrative actions.

```tsx
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);
  };
  // ...
}
```

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/program/partners/partnerId/layout.tsx:72-95](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/partners/%5BpartnerId%5D/layout.tsx#L72-L95)

## Navigation and Marketplace Routing

### Sidebar Navigation Integration

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.

Sources: [apps/web/ui/layout/sidebar/app-sidebar-nav.tsx:90-124](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/layout/sidebar/app-sidebar-nav.tsx#L90-L124)

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`.

Sources: [apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx:67-104](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx#L67-L104)

### Program Switching and Dropdown State

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`.

```tsx
  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]);
```

Sources: [apps/web/ui/layout/sidebar/partner-program-dropdown.tsx:31-44](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/layout/sidebar/partner-program-dropdown.tsx#L31-L44)

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.

```tsx
  const href = useCallback(
    (slug: string) =>
      selectedProgram
        ? `${pathname.replace(selectedProgram.slug, slug)}${searchParamsString.length > 0 ? `?${searchParamsString}` : ""}`
        : `/programs/${slug}`,
    [pathname, selectedProgram],
  );
```

Sources: [apps/web/ui/layout/sidebar/partner-program-dropdown.tsx:173-179](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/layout/sidebar/partner-program-dropdown.tsx#L173-L179)

### Middleware Routing Across Portal Subdomains

Incoming requests to partner portal subdomains are intercepted by `PartnersMiddleware`, which evaluates authentication tokens, path structures, and default partner identifiers.

```typescript
const AUTHENTICATED_PATHS = [
  "/programs",
  "/marketplace",
  "/onboarding",
  "/settings",
  "/profile",
  "/messages",
  "/payouts",
  "/account",
  "/invite",
  "/rewind",
];
```

Sources: [apps/web/lib/middleware/partners.ts:12-23](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/partners.ts#L12-L23)

The middleware executes an explicit validation and routing sequence:

1. `parse(req)` extracts the path, full path, and search parameters.
2. `getUserViaToken(req)` resolves the active session user.
3. If the path matches an authenticated route and no user exists, it redirects unauthenticated requests to `/login?next=...` (or `/[programSlug]/login` for custom program paths).
4. If a user exists but lacks a `defaultPartnerId` (and is not accessing `/onboarding`, `/account`, or an invite link), the middleware redirects them to `/onboarding`.
5. Validated internal redirects via `searchParamsObj.next` are processed securely while omitting onboarding routes.
6. Root and partner network routes (`/` or `/pn_*`) are rewritten or redirected to `/programs`, while all other requests are rewritten to the underlying `/partners.dub.co` subdomain.

Sources: [apps/web/lib/middleware/partners.ts:25-122](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/partners.ts#L25-L122)

## Related

- [Commission Rules and Rewards](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/commission-rules-and-rewards)
- [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.
