Affiliate & Referral Systems
Affiliate und Referral Programme implementieren. Tracking Links, Provisionsberechnung und Auszahlungen mit Node.js und Stripe automatisieren.

Affiliate & Referral Systems
Meta-Description: Affiliate und Referral Programme implementieren. Tracking Links, Provisionsberechnung und Auszahlungen mit Node.js und Stripe automatisieren.
Keywords: Affiliate Marketing, Referral Program, Commission Tracking, Partner Program, Referral Links, Payout System, Revenue Share
Einführung
Affiliate und Referral Systeme treiben Wachstum durch Empfehlungen. Von Tracking-Links über Commission-Berechnung bis automatisierte Payouts – ein gut implementiertes System skaliert Kundenakquise kosteneffizient. Dieser Guide zeigt Production-ready Implementierungen.
System Architecture
┌─────────────────────────────────────────────────────────────┐
│ AFFILIATE & REFERRAL SYSTEM │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ AFFILIATE │ │ REFERRAL │ │
│ │ (B2B/Partner) │ │ (Customer) │ │
│ ├─────────────────┤ ├─────────────────┤ │
│ │ • Influencers │ │ • Invite friends│ │
│ │ • Bloggers │ │ • Share links │ │
│ │ • Review sites │ │ • Earn rewards │ │
│ │ • Partners │ │ • Both benefit │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ └───────────┬───────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ TRACKING SYSTEM │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ Unique Link ──► Click Track ──► Cookie/Store │ │
│ │ │ │ │ │ │
│ │ ?ref=ABC123 IP, Device, 30-90 day │ │
│ │ Timestamp attribution │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ CONVERSION & COMMISSION │ │
│ │ │ │
│ │ Purchase ──► Attribution ──► Commission ──► Payout│ │
│ │ │ │ │ │ │ │
│ │ Order ID Match Cookie Calculate Monthly │ │
│ │ completed to Affiliate 20-50% Stripe │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Commission Models: │
│ ├── One-time: Single payment per conversion │
│ ├── Recurring: % of subscription for lifetime │
│ ├── Tiered: Higher rates at volume thresholds │
│ └── Hybrid: One-time + recurring combined │
│ │
└─────────────────────────────────────────────────────────────┘Database Schema
// prisma/schema.prisma
model Affiliate {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
// Affiliate identity
code String @unique // ref=ABC123
slug String? @unique // custom vanity URL
// Commission settings
commissionType CommissionType @default(PERCENTAGE)
commissionRate Decimal @default(20) // 20%
recurringRate Decimal? // For subscription commissions
// Tier (affects commission rates)
tier AffiliateTier @default(STANDARD)
// Payout info
payoutMethod PayoutMethod @default(STRIPE)
stripeAccountId String?
paypalEmail String?
minimumPayout Int @default(5000) // $50.00 minimum
// Status
status AffiliateStatus @default(PENDING)
approvedAt DateTime?
suspendedAt DateTime?
// Tracking
clicks Click[]
conversions Conversion[]
payouts Payout[]
// Stats (denormalized for performance)
totalClicks Int @default(0)
totalConversions Int @default(0)
totalEarnings Int @default(0) // cents
pendingBalance Int @default(0) // cents
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Click {
id String @id @default(cuid())
affiliateId String
affiliate Affiliate @relation(fields: [affiliateId], references: [id])
// Tracking data
ipAddress String?
userAgent String?
referer String?
landingPage String
// UTM parameters
utmSource String?
utmMedium String?
utmCampaign String?
// Visitor identification
visitorId String // Fingerprint or cookie ID
sessionId String?
// Conversion tracking
converted Boolean @default(false)
conversionId String?
createdAt DateTime @default(now())
@@index([affiliateId, createdAt])
@@index([visitorId])
}
model Conversion {
id String @id @default(cuid())
affiliateId String
affiliate Affiliate @relation(fields: [affiliateId], references: [id])
// Order info
orderId String @unique
customerId String
orderAmount Int // cents
currency String @default("EUR")
// Commission
commissionAmount Int // cents
commissionRate Decimal
commissionType CommissionType
// Status
status ConversionStatus @default(PENDING)
approvedAt DateTime?
rejectedAt DateTime?
rejectionReason String?
// Payout tracking
payoutId String?
payout Payout? @relation(fields: [payoutId], references: [id])
paidAt DateTime?
// Attribution
clickId String?
attributionWindow Int // days
// For recurring commissions
isRecurring Boolean @default(false)
parentConversionId String?
recurringMonth Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([affiliateId, status])
@@index([affiliateId, createdAt])
}
model Payout {
id String @id @default(cuid())
affiliateId String
affiliate Affiliate @relation(fields: [affiliateId], references: [id])
amount Int // cents
currency String @default("EUR")
fee Int @default(0) // Processing fee
method PayoutMethod
stripeTransferId String?
paypalPayoutId String?
status PayoutStatus @default(PENDING)
processedAt DateTime?
failedAt DateTime?
failureReason String?
conversions Conversion[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Referral-specific (customer referrals)
model Referral {
id String @id @default(cuid())
referrerId String // User who referred
referrer User @relation("ReferralsMade", fields: [referrerId], references: [id])
referredId String? // User who signed up
referred User? @relation("ReferralsReceived", fields: [referredId], references: [id])
code String @unique
email String? // Invited email (before signup)
// Rewards
referrerReward Int? // cents
referredReward Int? // cents
rewardType RewardType @default(CREDIT)
// Status
status ReferralStatus @default(PENDING)
signedUpAt DateTime?
qualifiedAt DateTime? // Met qualification criteria
rewardedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum CommissionType {
PERCENTAGE
FIXED
TIERED
}
enum AffiliateTier {
STANDARD // 20%
SILVER // 25%
GOLD // 30%
PLATINUM // 35%
}
enum AffiliateStatus {
PENDING
ACTIVE
SUSPENDED
TERMINATED
}
enum PayoutMethod {
STRIPE
PAYPAL
BANK_TRANSFER
}
enum ConversionStatus {
PENDING // Waiting for approval period
APPROVED // Commission confirmed
REJECTED // Refund, fraud, etc.
PAID // Included in payout
}
enum PayoutStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}
enum RewardType {
CREDIT // Account credit
CASH // Cash payout
DISCOUNT // Discount code
}
enum ReferralStatus {
PENDING // Invited, not signed up
SIGNED_UP // Created account
QUALIFIED // Met criteria (purchase, etc.)
REWARDED // Rewards distributed
EXPIRED // Never signed up
}Affiliate Link Generation
// lib/affiliate/links.ts
import { db } from '@/lib/db';
import crypto from 'crypto';
export async function generateAffiliateCode(): Promise<string> {
const code = crypto.randomBytes(4).toString('hex').toUpperCase();
// Ensure uniqueness
const existing = await db.affiliate.findUnique({
where: { code }
});
if (existing) {
return generateAffiliateCode(); // Retry
}
return code;
}
export function buildAffiliateLink(
baseUrl: string,
affiliateCode: string,
campaign?: string
): string {
const url = new URL(baseUrl);
url.searchParams.set('ref', affiliateCode);
if (campaign) {
url.searchParams.set('utm_campaign', campaign);
url.searchParams.set('utm_source', 'affiliate');
url.searchParams.set('utm_medium', 'referral');
}
return url.toString();
}
// Generate vanity URL
export async function setAffiliateSlug(
affiliateId: string,
slug: string
): Promise<boolean> {
// Validate slug format
if (!/^[a-z0-9-]{3,30}$/.test(slug)) {
throw new Error('Invalid slug format');
}
// Check reserved words
const reserved = ['admin', 'api', 'app', 'www', 'help', 'support'];
if (reserved.includes(slug)) {
throw new Error('Slug is reserved');
}
try {
await db.affiliate.update({
where: { id: affiliateId },
data: { slug }
});
return true;
} catch {
throw new Error('Slug already taken');
}
}
// Link resolver
export async function resolveAffiliateLink(
refCode: string
): Promise<Affiliate | null> {
// Try code first
let affiliate = await db.affiliate.findUnique({
where: { code: refCode, status: 'ACTIVE' }
});
// Try slug
if (!affiliate) {
affiliate = await db.affiliate.findUnique({
where: { slug: refCode, status: 'ACTIVE' }
});
}
return affiliate;
}Click Tracking
// lib/affiliate/tracking.ts
import { db } from '@/lib/db';
import { cookies } from 'next/headers';
import { NextRequest } from 'next/server';
import crypto from 'crypto';
const COOKIE_NAME = 'aff_ref';
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days
interface TrackClickParams {
affiliateId: string;
request: NextRequest;
landingPage: string;
}
export async function trackClick(params: TrackClickParams): Promise<Click> {
const { affiliateId, request, landingPage } = params;
const visitorId = getVisitorId(request);
const searchParams = new URL(request.url).searchParams;
const click = await db.click.create({
data: {
affiliateId,
ipAddress: request.headers.get('x-forwarded-for'),
userAgent: request.headers.get('user-agent'),
referer: request.headers.get('referer'),
landingPage,
utmSource: searchParams.get('utm_source'),
utmMedium: searchParams.get('utm_medium'),
utmCampaign: searchParams.get('utm_campaign'),
visitorId,
sessionId: crypto.randomUUID()
}
});
// Update affiliate stats
await db.affiliate.update({
where: { id: affiliateId },
data: { totalClicks: { increment: 1 } }
});
return click;
}
function getVisitorId(request: NextRequest): string {
// Try to get existing visitor ID from cookie
const existingId = request.cookies.get('visitor_id')?.value;
if (existingId) return existingId;
// Generate fingerprint from request headers
const components = [
request.headers.get('user-agent'),
request.headers.get('accept-language'),
request.headers.get('accept-encoding')
].filter(Boolean).join('|');
return crypto.createHash('sha256').update(components).digest('hex').slice(0, 16);
}
// Set affiliate cookie
export function setAffiliateCookie(affiliateCode: string): void {
const cookieStore = cookies();
cookieStore.set(COOKIE_NAME, affiliateCode, {
maxAge: COOKIE_MAX_AGE,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/'
});
}
// Get affiliate from cookie
export function getAffiliateFromCookie(): string | null {
const cookieStore = cookies();
return cookieStore.get(COOKIE_NAME)?.value || null;
}
// Attribution window check
export async function findAttributableClick(
visitorId: string,
affiliateId: string,
windowDays: number = 30
): Promise<Click | null> {
const windowStart = new Date();
windowStart.setDate(windowStart.getDate() - windowDays);
return db.click.findFirst({
where: {
affiliateId,
visitorId,
createdAt: { gte: windowStart },
converted: false
},
orderBy: { createdAt: 'desc' }
});
}Tracking Middleware
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const refCode = request.nextUrl.searchParams.get('ref');
if (refCode) {
// Validate affiliate exists and is active
const isValid = await validateAffiliateCode(refCode);
if (isValid) {
// Set cookie
response.cookies.set('aff_ref', refCode, {
maxAge: 30 * 24 * 60 * 60, // 30 days
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax'
});
// Track click asynchronously
trackClickAsync(refCode, request);
// Remove ref from URL for cleaner UX
const cleanUrl = new URL(request.url);
cleanUrl.searchParams.delete('ref');
if (cleanUrl.toString() !== request.url) {
return NextResponse.redirect(cleanUrl);
}
}
}
return response;
}
async function validateAffiliateCode(code: string): Promise<boolean> {
// Fast validation via edge-compatible check
const response = await fetch(
`${process.env.NEXT_PUBLIC_APP_URL}/api/affiliate/validate?code=${code}`
);
return response.ok;
}
function trackClickAsync(refCode: string, request: NextRequest): void {
// Fire and forget
fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/affiliate/track`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refCode,
landingPage: request.nextUrl.pathname,
userAgent: request.headers.get('user-agent'),
referer: request.headers.get('referer'),
ip: request.headers.get('x-forwarded-for')
})
}).catch(console.error);
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
};Commission Calculation
// lib/affiliate/commission.ts
import { db } from '@/lib/db';
import { Decimal } from '@prisma/client/runtime/library';
interface CommissionResult {
amount: number;
rate: Decimal;
type: CommissionType;
}
// Tier-based commission rates
const TIER_RATES: Record<AffiliateTier, number> = {
STANDARD: 20,
SILVER: 25,
GOLD: 30,
PLATINUM: 35
};
export function calculateCommission(
orderAmount: number,
affiliate: Affiliate
): CommissionResult {
let rate: Decimal;
let amount: number;
switch (affiliate.commissionType) {
case 'PERCENTAGE':
rate = affiliate.tier
? new Decimal(TIER_RATES[affiliate.tier])
: affiliate.commissionRate;
amount = Math.floor(orderAmount * rate.toNumber() / 100);
break;
case 'FIXED':
rate = affiliate.commissionRate;
amount = rate.toNumber() * 100; // Convert to cents
break;
case 'TIERED':
const tierResult = calculateTieredCommission(orderAmount, affiliate);
rate = tierResult.rate;
amount = tierResult.amount;
break;
default:
rate = new Decimal(20);
amount = Math.floor(orderAmount * 0.2);
}
return {
amount,
rate,
type: affiliate.commissionType
};
}
function calculateTieredCommission(
orderAmount: number,
affiliate: Affiliate
): { amount: number; rate: Decimal } {
// Get affiliate's total conversions this month
const monthStart = new Date();
monthStart.setDate(1);
monthStart.setHours(0, 0, 0, 0);
// Tiered rates based on monthly volume
const tiers = [
{ threshold: 10000_00, rate: 20 }, // $10k+ = 20%
{ threshold: 5000_00, rate: 25 }, // $5k+ = 25%
{ threshold: 1000_00, rate: 30 }, // $1k+ = 30%
{ threshold: 0, rate: 35 } // Base = 35%
];
// For simplicity, use the affiliate's total earnings to determine tier
const volumeTier = tiers.find(t => affiliate.totalEarnings >= t.threshold);
const rate = volumeTier?.rate || 20;
return {
amount: Math.floor(orderAmount * rate / 100),
rate: new Decimal(rate)
};
}
// Calculate recurring commission for subscriptions
export function calculateRecurringCommission(
subscriptionAmount: number,
affiliate: Affiliate,
month: number
): CommissionResult | null {
// Most programs cap recurring at 12 months
if (month > 12) return null;
// Recurring rate might be lower than initial
const recurringRate = affiliate.recurringRate || affiliate.commissionRate;
const amount = Math.floor(subscriptionAmount * recurringRate.toNumber() / 100);
return {
amount,
rate: recurringRate,
type: 'PERCENTAGE'
};
}Conversion Recording
// lib/affiliate/conversions.ts
import { db } from '@/lib/db';
import { calculateCommission } from './commission';
import { getAffiliateFromCookie } from './tracking';
interface RecordConversionParams {
orderId: string;
customerId: string;
orderAmount: number;
currency?: string;
isSubscription?: boolean;
}
export async function recordConversion(
params: RecordConversionParams
): Promise<Conversion | null> {
const { orderId, customerId, orderAmount, currency = 'EUR', isSubscription } = params;
// Check for existing conversion (prevent duplicates)
const existing = await db.conversion.findUnique({
where: { orderId }
});
if (existing) return existing;
// Get affiliate from cookie or customer's first touch
const affiliateCode = getAffiliateFromCookie();
if (!affiliateCode) return null;
const affiliate = await db.affiliate.findUnique({
where: { code: affiliateCode, status: 'ACTIVE' }
});
if (!affiliate) return null;
// Calculate commission
const commission = calculateCommission(orderAmount, affiliate);
// Find attributable click
const click = await db.click.findFirst({
where: {
affiliateId: affiliate.id,
converted: false,
createdAt: {
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // 30 days
}
},
orderBy: { createdAt: 'desc' }
});
// Create conversion
const conversion = await db.$transaction(async (tx) => {
// Create conversion record
const conv = await tx.conversion.create({
data: {
affiliateId: affiliate.id,
orderId,
customerId,
orderAmount,
currency,
commissionAmount: commission.amount,
commissionRate: commission.rate,
commissionType: commission.type,
clickId: click?.id,
attributionWindow: 30,
isRecurring: isSubscription
}
});
// Mark click as converted
if (click) {
await tx.click.update({
where: { id: click.id },
data: { converted: true, conversionId: conv.id }
});
}
// Update affiliate stats
await tx.affiliate.update({
where: { id: affiliate.id },
data: {
totalConversions: { increment: 1 },
pendingBalance: { increment: commission.amount }
}
});
return conv;
});
return conversion;
}
// Auto-approve conversions after hold period
export async function approveConversions(): Promise<number> {
const holdPeriod = 14; // days
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - holdPeriod);
const pendingConversions = await db.conversion.findMany({
where: {
status: 'PENDING',
createdAt: { lte: cutoffDate }
}
});
let approved = 0;
for (const conversion of pendingConversions) {
// Check if order was refunded
const wasRefunded = await checkOrderRefunded(conversion.orderId);
if (wasRefunded) {
await db.conversion.update({
where: { id: conversion.id },
data: {
status: 'REJECTED',
rejectedAt: new Date(),
rejectionReason: 'Order refunded'
}
});
// Reduce pending balance
await db.affiliate.update({
where: { id: conversion.affiliateId },
data: { pendingBalance: { decrement: conversion.commissionAmount } }
});
} else {
await db.conversion.update({
where: { id: conversion.id },
data: {
status: 'APPROVED',
approvedAt: new Date()
}
});
// Move from pending to earned
await db.affiliate.update({
where: { id: conversion.affiliateId },
data: {
pendingBalance: { decrement: conversion.commissionAmount },
totalEarnings: { increment: conversion.commissionAmount }
}
});
approved++;
}
}
return approved;
}
async function checkOrderRefunded(orderId: string): Promise<boolean> {
// Check with payment provider
const order = await db.order.findUnique({
where: { id: orderId }
});
return order?.status === 'REFUNDED';
}Payout System
// lib/affiliate/payouts.ts
import { db } from '@/lib/db';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
interface ProcessPayoutsResult {
processed: number;
failed: number;
totalAmount: number;
}
export async function processPayouts(): Promise<ProcessPayoutsResult> {
const result = { processed: 0, failed: 0, totalAmount: 0 };
// Find affiliates with approved conversions ready for payout
const affiliatesWithBalance = await db.affiliate.findMany({
where: {
status: 'ACTIVE',
totalEarnings: { gte: db.affiliate.fields.minimumPayout }
},
include: {
conversions: {
where: {
status: 'APPROVED',
payoutId: null
}
}
}
});
for (const affiliate of affiliatesWithBalance) {
if (affiliate.conversions.length === 0) continue;
const payoutAmount = affiliate.conversions.reduce(
(sum, c) => sum + c.commissionAmount,
0
);
if (payoutAmount < affiliate.minimumPayout) continue;
try {
const payout = await createPayout(affiliate, payoutAmount);
// Link conversions to payout
await db.conversion.updateMany({
where: {
id: { in: affiliate.conversions.map(c => c.id) }
},
data: {
payoutId: payout.id,
status: 'PAID',
paidAt: new Date()
}
});
// Reset affiliate balance
await db.affiliate.update({
where: { id: affiliate.id },
data: { totalEarnings: { decrement: payoutAmount } }
});
result.processed++;
result.totalAmount += payoutAmount;
} catch (error) {
console.error(`Payout failed for ${affiliate.id}:`, error);
result.failed++;
}
}
return result;
}
async function createPayout(
affiliate: Affiliate,
amount: number
): Promise<Payout> {
// Create payout record
const payout = await db.payout.create({
data: {
affiliateId: affiliate.id,
amount,
currency: 'EUR',
method: affiliate.payoutMethod,
status: 'PROCESSING'
}
});
try {
switch (affiliate.payoutMethod) {
case 'STRIPE':
await processStripePayout(affiliate, payout);
break;
case 'PAYPAL':
await processPayPalPayout(affiliate, payout);
break;
default:
throw new Error('Unsupported payout method');
}
await db.payout.update({
where: { id: payout.id },
data: {
status: 'COMPLETED',
processedAt: new Date()
}
});
} catch (error) {
await db.payout.update({
where: { id: payout.id },
data: {
status: 'FAILED',
failedAt: new Date(),
failureReason: error instanceof Error ? error.message : 'Unknown error'
}
});
throw error;
}
return payout;
}
async function processStripePayout(
affiliate: Affiliate,
payout: Payout
): Promise<void> {
if (!affiliate.stripeAccountId) {
throw new Error('No Stripe account connected');
}
const transfer = await stripe.transfers.create({
amount: payout.amount,
currency: payout.currency.toLowerCase(),
destination: affiliate.stripeAccountId,
metadata: {
payoutId: payout.id,
affiliateId: affiliate.id
}
});
await db.payout.update({
where: { id: payout.id },
data: { stripeTransferId: transfer.id }
});
}
async function processPayPalPayout(
affiliate: Affiliate,
payout: Payout
): Promise<void> {
if (!affiliate.paypalEmail) {
throw new Error('No PayPal email configured');
}
// PayPal Payouts API integration
const paypalPayout = await createPayPalPayout({
email: affiliate.paypalEmail,
amount: (payout.amount / 100).toFixed(2),
currency: payout.currency,
note: `Affiliate commission payout #${payout.id}`
});
await db.payout.update({
where: { id: payout.id },
data: { paypalPayoutId: paypalPayout.batch_id }
});
}Customer Referral System
// lib/referral/referrals.ts
import { db } from '@/lib/db';
import crypto from 'crypto';
const REFERRAL_REWARDS = {
referrer: 1000, // $10 credit
referred: 500 // $5 credit
};
export async function createReferralCode(userId: string): Promise<string> {
const existing = await db.referral.findFirst({
where: { referrerId: userId }
});
if (existing) return existing.code;
const code = crypto.randomBytes(4).toString('hex').toUpperCase();
await db.referral.create({
data: {
referrerId: userId,
code,
status: 'PENDING'
}
});
return code;
}
export async function processReferralSignup(
newUserId: string,
referralCode: string
): Promise<boolean> {
const referral = await db.referral.findUnique({
where: { code: referralCode }
});
if (!referral || referral.status !== 'PENDING') {
return false;
}
// Prevent self-referral
if (referral.referrerId === newUserId) {
return false;
}
await db.referral.update({
where: { id: referral.id },
data: {
referredId: newUserId,
status: 'SIGNED_UP',
signedUpAt: new Date()
}
});
return true;
}
export async function qualifyReferral(
referredUserId: string
): Promise<void> {
const referral = await db.referral.findFirst({
where: {
referredId: referredUserId,
status: 'SIGNED_UP'
}
});
if (!referral) return;
await db.$transaction([
// Update referral status
db.referral.update({
where: { id: referral.id },
data: {
status: 'QUALIFIED',
qualifiedAt: new Date(),
referrerReward: REFERRAL_REWARDS.referrer,
referredReward: REFERRAL_REWARDS.referred
}
}),
// Credit referrer
db.user.update({
where: { id: referral.referrerId },
data: { credit: { increment: REFERRAL_REWARDS.referrer } }
}),
// Credit referred user
db.user.update({
where: { id: referredUserId },
data: { credit: { increment: REFERRAL_REWARDS.referred } }
})
]);
// Send notifications
await notifyReferralReward(referral.referrerId, REFERRAL_REWARDS.referrer);
await notifyReferralReward(referredUserId, REFERRAL_REWARDS.referred);
}Affiliate Dashboard API
// app/api/affiliate/dashboard/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
export async function GET(request: NextRequest) {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const affiliate = await db.affiliate.findUnique({
where: { userId: session.user.id }
});
if (!affiliate) {
return NextResponse.json({ error: 'Not an affiliate' }, { status: 404 });
}
// Get date range
const { searchParams } = new URL(request.url);
const range = searchParams.get('range') || '30d';
const startDate = getStartDate(range);
// Fetch stats
const [clicks, conversions, recentConversions] = await Promise.all([
db.click.count({
where: {
affiliateId: affiliate.id,
createdAt: { gte: startDate }
}
}),
db.conversion.aggregate({
where: {
affiliateId: affiliate.id,
createdAt: { gte: startDate }
},
_count: true,
_sum: { commissionAmount: true, orderAmount: true }
}),
db.conversion.findMany({
where: { affiliateId: affiliate.id },
orderBy: { createdAt: 'desc' },
take: 10,
select: {
id: true,
orderAmount: true,
commissionAmount: true,
status: true,
createdAt: true
}
})
]);
// Calculate conversion rate
const conversionRate = clicks > 0
? ((conversions._count || 0) / clicks * 100).toFixed(2)
: '0.00';
return NextResponse.json({
affiliate: {
code: affiliate.code,
tier: affiliate.tier,
commissionRate: affiliate.commissionRate,
status: affiliate.status
},
stats: {
clicks,
conversions: conversions._count || 0,
conversionRate: `${conversionRate}%`,
revenue: conversions._sum.orderAmount || 0,
earnings: conversions._sum.commissionAmount || 0,
pendingBalance: affiliate.pendingBalance,
totalEarnings: affiliate.totalEarnings
},
recentConversions,
links: {
main: `${process.env.NEXT_PUBLIC_APP_URL}?ref=${affiliate.code}`,
pricing: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?ref=${affiliate.code}`
}
});
}
function getStartDate(range: string): Date {
const now = new Date();
switch (range) {
case '7d': return new Date(now.setDate(now.getDate() - 7));
case '30d': return new Date(now.setDate(now.getDate() - 30));
case '90d': return new Date(now.setDate(now.getDate() - 90));
case 'ytd': return new Date(now.getFullYear(), 0, 1);
default: return new Date(now.setDate(now.getDate() - 30));
}
}Best Practices
| Aspect | Recommendation |
|---|---|
| **Attribution** | 30-90 day cookie window |
| **Hold Period** | 14-30 days before approval |
| **Commission** | 20-50% for digital products |
| **Minimum Payout** | $50-100 threshold |
| **Fraud Prevention** | IP tracking, refund checks |
| **Communication** | Real-time dashboard, email alerts |
Fazit
Affiliate & Referral Systems erfordern:
- Tracking: Cookies, Attribution-Windows
- Commission: Faire, klare Berechnung
- Payouts: Automatisiert, zuverlässig
- Dashboard: Transparenz für Partner
Gute Partner-Programme wachsen organisch durch zufriedene Affiliates.
Bildprompts
- "Affiliate dashboard interface, earnings chart and conversion metrics"
- "Referral flow diagram, invite friend reward visualization"
- "Commission payout system, money transfer illustration"