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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions