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
Tracking & Attribution
Commissions
Payouts
Affiliate Portal
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
- Setup —
affiliateBackendPlugin + affiliateClientPlugin
- Attribution model — cookie/query tracking and attribution window
- Commission calculation — percent vs flat examples
- Portal usage — affiliate dashboard + referral link generation
- Schema reference —
AutoTypeTable for config + hooks
- Routes — route key/path table for admin + portal pages
Related Issues
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
pending->approved->suspendedTracking & Attribution
ref,affiliate, etc.)Commissions
pending->approved->paid/voidedPayouts
Affiliate Portal
Schema
Plugin Structure
Routes
affiliates/affiliate/admin/affiliatesaffiliateDetail/affiliate/admin/affiliates/:idcommissions/affiliate/admin/commissionspayouts/affiliate/admin/payoutsapply/affiliate/applyportal/affiliate/portalAttribution API
Hooks
Consumer Setup
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)
Plugin Configuration Options
attributionWindowDaysnumber30)attributionModedefaultCommissionTypedefaultCommissionValuenumberhooksAffiliatePluginHooksDocumentation
Add
docs/content/docs/plugins/affiliate.mdxcovering:affiliateBackendPlugin+affiliateClientPluginAutoTypeTablefor config + hooksRelated Issues