Skip to content

Affiliate Plugin #103

@olliethedev

Description

@olliethedev

Overview

Add an Affiliate plugin that enables partner/referral marketing with trackable links, commission rules, conversion attribution, and payout workflows. The plugin should provide both admin tools (program management, approvals, commissions, payouts) and lightweight consumer-facing helpers (affiliate signup, dashboard, and tracking link generation).

The goal is a practical v1 that covers end-to-end affiliate operations without locking users into any specific payment processor.


Core Features

Affiliate Management

  • Affiliate application + approval flow
  • Affiliate profile CRUD (name, email, status, payout details)
  • Affiliate status lifecycle: pending -> approved -> suspended
  • Unique affiliate codes and custom referral slugs

Tracking & Attribution

  • Track referral clicks from affiliate links
  • Cookie + query-param attribution (ref, affiliate, etc.)
  • Configurable attribution window (e.g. 30 days)
  • First-touch / last-touch attribution mode toggle
  • Conversion attribution to orders/events (manual event API + e-commerce integration)

Commissions

  • Commission rules: flat amount or percentage
  • Optional per-affiliate override rules
  • Commission statuses: pending -> approved -> paid / voided
  • Auto-calculate commission on attributed conversion

Payouts

  • Payout batch creation for approved commissions
  • Manual payout marking (v1), adapter-based automation later
  • Payout history per affiliate
  • CSV export for accounting

Affiliate Portal

  • Affiliate dashboard: clicks, conversions, commission totals, payout history
  • Generate/copy referral links for specific landing pages
  • Basic marketing assets section (link templates, banners metadata)

Schema

import { createDbPlugin } from "@btst/stack/plugins/api"

export const affiliateSchema = createDbPlugin("affiliate", {
  affiliate: {
    modelName: "affiliate",
    fields: {
      name:            { type: "string",  required: true },
      email:           { type: "string",  required: true },
      code:            { type: "string",  required: true },   // public referral code
      status:          { type: "string",  defaultValue: "pending" }, // "pending" | "approved" | "suspended"
      payoutDetails:   { type: "string",  required: false },  // JSON (paypal/bank/crypto/etc)
      notes:           { type: "string",  required: false },
      createdAt:       { type: "date",    defaultValue: () => new Date() },
      updatedAt:       { type: "date",    defaultValue: () => new Date() },
    },
  },
  referralClick: {
    modelName: "referralClick",
    fields: {
      affiliateId:     { type: "string",  required: true },
      code:            { type: "string",  required: true },
      landingPath:     { type: "string",  required: false },
      referrer:        { type: "string",  required: false },
      ipHash:          { type: "string",  required: false },
      userAgent:       { type: "string",  required: false },
      createdAt:       { type: "date",    defaultValue: () => new Date() },
    },
  },
  conversion: {
    modelName: "conversion",
    fields: {
      affiliateId:     { type: "string",  required: true },
      clickId:         { type: "string",  required: false },
      externalRef:     { type: "string",  required: false }, // order ID / event ID
      amount:          { type: "number",  required: true },
      currency:        { type: "string",  defaultValue: "USD" },
      status:          { type: "string",  defaultValue: "pending" }, // "pending" | "approved" | "rejected"
      createdAt:       { type: "date",    defaultValue: () => new Date() },
    },
  },
  commission: {
    modelName: "commission",
    fields: {
      affiliateId:     { type: "string",  required: true },
      conversionId:    { type: "string",  required: true },
      amount:          { type: "number",  required: true },
      currency:        { type: "string",  defaultValue: "USD" },
      status:          { type: "string",  defaultValue: "pending" }, // "pending" | "approved" | "paid" | "voided"
      createdAt:       { type: "date",    defaultValue: () => new Date() },
      updatedAt:       { type: "date",    defaultValue: () => new Date() },
    },
  },
  payout: {
    modelName: "payout",
    fields: {
      affiliateId:     { type: "string",  required: true },
      amount:          { type: "number",  required: true },
      currency:        { type: "string",  defaultValue: "USD" },
      status:          { type: "string",  defaultValue: "pending" }, // "pending" | "sent" | "failed"
      reference:       { type: "string",  required: false },
      paidAt:          { type: "date",    required: false },
      createdAt:       { type: "date",    defaultValue: () => new Date() },
    },
  },
})

Plugin Structure

src/plugins/affiliate/
├── db.ts
├── types.ts
├── schemas.ts
├── attribution.ts              # cookie/query attribution + matching logic
├── commission.ts               # commission calculation engine
├── query-keys.ts
├── client.css
├── style.css
├── api/
│   ├── plugin.ts               # defineBackendPlugin — affiliate, tracking, commission, payout endpoints
│   ├── getters.ts              # listAffiliates, getAffiliateStats, listCommissions, listPayouts
│   ├── mutations.ts            # approveAffiliate, trackClick, createConversion, approveCommission, createPayout
│   ├── query-key-defs.ts
│   ├── serializers.ts
│   └── index.ts
└── client/
    ├── plugin.tsx              # defineClientPlugin — admin + affiliate portal routes
    ├── overrides.ts            # AffiliatePluginOverrides
    ├── index.ts
    ├── hooks/
    │   ├── use-affiliate.tsx   # useAffiliateDashboard, useCommissions, usePayouts
    │   └── index.tsx
    └── components/
        └── pages/
            ├── affiliates-page.tsx / .internal.tsx
            ├── affiliate-detail-page.tsx / .internal.tsx
            ├── commissions-page.tsx / .internal.tsx
            ├── payouts-page.tsx / .internal.tsx
            ├── affiliate-portal-page.tsx / .internal.tsx
            └── affiliate-apply-page.tsx / .internal.tsx

Routes

Route Path Description
affiliates /affiliate/admin/affiliates Affiliate list + approval workflow
affiliateDetail /affiliate/admin/affiliates/:id Affiliate profile + stats
commissions /affiliate/admin/commissions Commission review and approval
payouts /affiliate/admin/payouts Payout batches + history
apply /affiliate/apply Public affiliate application page
portal /affiliate/portal Affiliate self-serve dashboard

Attribution API

// Public tracking pixel / redirect endpoint
GET /api/data/affiliate/track/:code?to=/landing/page

// Server-side conversion capture (e.g. checkout success)
await myStack.api.affiliate.createConversion({
  externalRef: "order_123",
  amount: 12900,
  currency: "USD",
  attribution: {
    code: "partner-jane",
  },
})

Hooks

affiliateBackendPlugin({
  attributionWindowDays?: number                                       // default: 30
  attributionMode?: "first_touch" | "last_touch"                     // default: "last_touch"
  defaultCommissionType?: "percent" | "flat"                         // default: "percent"
  defaultCommissionValue?: number                                      // e.g. 20 (%), or 1000 (cents)
  onBeforeApproveAffiliate?: (affiliate, ctx) => Promise<void>
  onAfterConversion?: (conversion, commission, ctx) => Promise<void>
  onBeforePayout?: (batch, ctx) => Promise<void>
  onAfterPayout?: (payout, ctx) => Promise<void>
})

Consumer Setup

// lib/stack.ts
import { affiliateBackendPlugin } from "@btst/stack/plugins/affiliate/api"

affiliate: affiliateBackendPlugin({
  attributionWindowDays: 30,
  attributionMode: "last_touch",
  defaultCommissionType: "percent",
  defaultCommissionValue: 20,
})
// lib/stack-client.tsx
import { affiliateClientPlugin } from "@btst/stack/plugins/affiliate/client"

affiliate: affiliateClientPlugin({
  apiBaseURL: "",
  apiBasePath: "/api/data",
  siteBasePath: "/pages",
  queryClient,
})

SSG Support

Affiliate admin and portal routes are user-specific and should be dynamic (force-dynamic). Public landing pages that read referral query params can remain static while attribution is recorded via tracking endpoint/cookie.


Non-Goals (v1)

  • Multi-level referral trees
  • Fraud detection / anti-abuse scoring
  • Automated tax forms (W-8/W-9)
  • Built-in payout processor automation (manual payout marking only)
  • Multi-currency settlement logic

Plugin Configuration Options

Option Type Description
attributionWindowDays number Referral attribution window (default: 30)
attributionMode `"first_touch" "last_touch"`
defaultCommissionType `"percent" "flat"`
defaultCommissionValue number Percent value or flat cents amount
hooks AffiliatePluginHooks Lifecycle hooks

Documentation

Add docs/content/docs/plugins/affiliate.mdx covering:

  • Overview — affiliates, attribution, commissions, payouts
  • SetupaffiliateBackendPlugin + affiliateClientPlugin
  • Attribution model — cookie/query tracking and attribution window
  • Commission calculation — percent vs flat examples
  • Portal usage — affiliate dashboard + referral link generation
  • Schema referenceAutoTypeTable for config + hooks
  • Routes — route key/path table for admin + portal pages

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions