You're building a SaaS. You want affiliates. You sign up for a third-party platform, paste in an embed snippet, and your program is live in 20 minutes.
Six months later you decide to switch tools — better pricing, a feature you need, or the platform gets acquired and the product direction changes. You export your CSV. And then you realize what you've actually lost.
The CSV has affiliate names, emails, and a payout history. What it doesn't have: the actual tracking infrastructure. The referral links are dead. The cookie logic lived on their servers. The conversion attribution pipeline is gone. You don't just migrate data — you rebuild from scratch. Every affiliate needs a new link. Every integration needs to be rewired. The "switch" that should take a weekend takes a month.
This is the core problem with third-party affiliate platforms that nobody writes about. It's not just the monthly fee. It's that switching costs compound invisibly until the day you try to leave.
The Real Cost of Third-Party Affiliate Platforms
Your Data Is Theirs Until You Leave
When you run your affiliate program on a closed-source platform, your conversion events, click logs, affiliate relationships, and commission history all live in their database schema. You get access through their UI and, if you're lucky, a reasonably complete API.
The moment you stop paying, access is revoked. The CSV they give you is a snapshot — flat, denormalized, stripped of relational context. You lose:
- Click-level attribution data — which specific link, which campaign, which landing page variant drove each conversion
- Time-series granularity — daily click and conversion curves that let you spot seasonal patterns or campaign spikes
- Custom metadata — any extra fields you attached to affiliate signups or conversion events
- The tracking layer itself — the referral link format, the cookie domain, the JavaScript snippet. All of it belongs to them. You can export the ledger. You cannot export the engine.
Switching Means Starting Over
Here's what a migration from a third-party affiliate platform actually looks like in practice:
-
Referral links break immediately. Every affiliate has been sharing
yourplatform.com/ref/affiliate-slugstyle links that route through the platform's redirect infrastructure. Those links die the moment you cancel. Your affiliates are now sending traffic to dead URLs. - Attribution history is orphaned. Even if you have the CSV, your new system has no way to resolve historical conversions back to the correct affiliate unless you manually re-import and remap every record. Most platforms export commission totals, not the raw conversion events that produced them.
- Affiliate trust takes a hit. You have to email every active affiliate, explain the migration, give them new links, and ask them to update everything they've published — blog posts, YouTube descriptions, newsletters. Some won't. Those referrals are permanently lost.
- You rebuild the integration anyway. The Stripe webhook handler, the checkout metadata, the cookie capture logic — you write all of it fresh regardless of which new platform you move to. The only question is whether you're writing it to feed another third-party system or your own.
The Lock-In Is Structural, Not Accidental
This isn't a bug in how these platforms work. It's the business model. The harder you are to leave, the more pricing power they have over you. A platform that makes migration easy is a platform that competes on merit every renewal cycle. Most don't want that.
Self-hosted affiliate tracking software eliminates this dynamic entirely. Your data is in your database, in your schema, accessible to your queries. Your tracking logic is in your codebase. Switching tools means updating a config, not rebuilding an affiliate program.
How Affiliate Tracking Actually Works Under the Hood
Before you self-host anything, you need a clear mental model of what "affiliate tracking" actually consists of. It's three distinct components:
1. Attribution — Capturing the Referral
When a visitor arrives via an affiliate link (yourapp.com?ref=affiliate-slug), something needs to capture that ref parameter and persist it until conversion. The standard approach: read the query param, write it to a cookie with a 30–90 day expiration.
// Simplified attribution logic — runs when a visitor lands
function captureAffiliateRef(searchParams: URLSearchParams): void {
const ref = searchParams.get('ref');
if (!ref || !isValidSlug(ref)) return;
// Only set on first touch — first-click attribution
if (document.cookie.includes('affiliate_ref=')) return;
const maxAge = 60 * 60 * 24 * 30; // 30 days
document.cookie = `affiliate_ref=${encodeURIComponent(ref)}; max-age=${maxAge}; path=/; SameSite=Lax`;
}
function isValidSlug(slug: string): boolean {
return /^[a-zA-Z0-9][a-zA-Z0-9-]{1,39}$/.test(slug);
}
First-click attribution (shown above) gives credit to the first affiliate who drove the visit. The cookie isn't overwritten if it already exists. This is the most common and fair model for SaaS affiliate programs.
2. Conversion — Linking the Payment to the Referral
When the user completes a paid action, you read the cookie and attach the affiliate reference to the payment event. With Stripe, this means passing it as session metadata at checkout creation, then reading it back in the webhook.
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20',
});
export async function POST(request: NextRequest): Promise<NextResponse> {
// Read the affiliate cookie from the incoming request
const affiliateRef = request.cookies.get('affiliate_ref')?.value ?? null;
const sessionParams: Stripe.Checkout.SessionCreateParams = {
mode: 'subscription',
line_items: [{ price: process.env.STRIPE_PRICE_ID!, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
};
if (affiliateRef) {
// Attach to session — readable in checkout.session.completed webhook
sessionParams.metadata = { affiliate_ref: affiliateRef };
// Attach to subscription — readable on every invoice for recurring commissions
sessionParams.subscription_data = {
metadata: { affiliate_ref: affiliateRef },
};
}
const session = await stripe.checkout.sessions.create(sessionParams);
return NextResponse.json({ url: session.url });
}
Why attach to both session and subscription?
The checkout.session.completed event fires once. But if you're paying recurring commissions (e.g., 20% of every renewal), you need the ref on the subscription object so it's available on every invoice.payment_succeeded event going forward.
3. Confirmation — Webhook-Driven Commission Recording
Never trust the client-side success page for conversion confirmation. Users close tabs, skip redirects, or hit back. The Stripe webhook is the authoritative signal.
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20',
});
export async function POST(request: NextRequest): Promise<NextResponse> {
const body = await request.text();
const sig = request.headers.get('stripe-signature');
if (!sig) return NextResponse.json({ error: 'No signature' }, { status: 400 });
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
// Initial conversion
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session;
const ref = session.metadata?.affiliate_ref;
if (ref && session.amount_total) {
await db.affiliateConversion.create({
data: {
affiliateSlug: ref,
stripeSessionId: session.id,
stripeCustomerId: session.customer as string,
amountCents: session.amount_total,
commissionCents: Math.floor(session.amount_total * 0.2), // 20% commission
status: 'pending',
convertedAt: new Date(),
},
});
}
}
// Recurring commissions
if (event.type === 'invoice.payment_succeeded') {
const invoice = event.data.object as Stripe.Invoice;
const sub = await stripe.subscriptions.retrieve(invoice.subscription as string);
const ref = sub.metadata?.affiliate_ref;
if (ref && invoice.amount_paid) {
await db.affiliateCommission.create({
data: {
affiliateSlug: ref,
stripeInvoiceId: invoice.id,
amountCents: invoice.amount_paid,
commissionCents: Math.floor(invoice.amount_paid * 0.2),
type: 'recurring',
status: 'pending',
paidAt: new Date(),
},
});
}
}
return NextResponse.json({ received: true });
}
The Database Schema You Actually Own
This is what your data looks like when it lives in your own Postgres instance — queryable, JOIN-able, fully yours:
-- Affiliates
CREATE TABLE affiliates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT UNIQUE NOT NULL, -- The ?ref= value
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
commission_rate NUMERIC(5,4) DEFAULT 0.20, -- 20%
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- One-time conversions
CREATE TABLE affiliate_conversions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
affiliate_id UUID NOT NULL REFERENCES affiliates(id),
stripe_session_id TEXT UNIQUE NOT NULL,
stripe_customer_id TEXT NOT NULL,
amount_cents INTEGER NOT NULL,
commission_cents INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', -- pending | paid | reversed
converted_at TIMESTAMPTZ NOT NULL,
paid_at TIMESTAMPTZ
);
-- Recurring commission events
CREATE TABLE affiliate_commissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
affiliate_id UUID NOT NULL REFERENCES affiliates(id),
stripe_invoice_id TEXT UNIQUE NOT NULL,
amount_cents INTEGER NOT NULL,
commission_cents INTEGER NOT NULL,
type TEXT NOT NULL DEFAULT 'recurring',
status TEXT NOT NULL DEFAULT 'pending',
paid_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_conversions_affiliate ON affiliate_conversions(affiliate_id);
CREATE INDEX idx_commissions_affiliate ON affiliate_commissions(affiliate_id);
CREATE INDEX idx_conversions_status ON affiliate_conversions(status);
When your data is in a schema you control, cross-referencing affiliate performance against churn, LTV, or plan type is a SQL query, not a support ticket.
The Build-vs-Deploy Calculation
Here's the honest breakdown of what building affiliate tracking yourself actually costs:
| Component | DIY Effort | Notes |
|---|---|---|
| Cookie capture logic | ~1 hour | Straightforward, shown above |
| Stripe checkout integration | ~2 hours | Shown above |
| Stripe webhook handler | ~2 hours | Shown above |
| Database schema + migrations | ~1 hour | Shown above |
| Affiliate-facing portal | 80–150 hours | The part you don't want to build |
| Payout management UI | 40–80 hours | Approve, reject, mark as paid |
| Email notifications | 20–40 hours | Commission approved, payout sent |
The tracking code is 8 hours of work. The surrounding infrastructure — the portal your affiliates actually log into, the payout queue, the notification emails — is where real time disappears. That's not a reason to use a third-party platform. It's a reason to use open-source self-hosted infrastructure that ships all of it already built.
Where RefearnApp Comes In
RefearnApp is a free, open-source, self-hostable affiliate tracking platform built for exactly this stack — Next.js and Stripe — that gives you the 80–150 hours of infrastructure work without the 80–150 hours.
What it includes out of the box:
- Affiliate portal — Your affiliates get a real dashboard to track their clicks, conversions, and commission history. You don't build this.
- Commission engine — Configurable percentage and flat-rate commissions, with recurring attribution for subscription billing built in.
- Payout tracking — Mark commissions as pending, approved, or paid. Full payout history per affiliate.
- Zero revenue cut — No percentage rake on commissions. Every dollar goes to your affiliates.
- Your database, your schema — When you deploy RefearnApp, the data is in your Postgres instance. You can query it, back it up, migrate it, or extend it whenever you want. No CSV exports. No support tickets. No lock-in.
The critical difference from third-party platforms: if you ever need to change tools, extend functionality, or migrate to a different payment processor, you're modifying TypeScript in a repo you own. Not starting over.
Summary
Third-party affiliate platforms don't just cost money. They cost optionality. The moment you decide to switch — and eventually you will — you discover that the real asset wasn't the dashboard or the payout UI. It was the tracking layer underneath: the referral links, the cookie logic, the attribution pipeline. All of it belongs to them.
Self-hosted affiliate tracking software means:
- Your referral links are on your domain. They don't break when you change tools.
- Your conversion data is in your database. You query it, you own it, you migrate it.
- Your tracking logic is in your codebase. You read it, you modify it, you extend it.
- Switching costs drop to near zero. You update config, not rebuild a program.
Conclusion & Next Steps
Take Control of Your Affiliate Data
RefearnApp is fully open-source, self-hostable, and AGPL-3.0 licensed. You can deploy it completely free on your own infrastructure using Coolify or check out our direct paths:
- Developer Choice: Read Deployment Docs or ⭐ Star the Repo to run it on your own VPS via Coolify with 100% data ownership.
- Production Ready: Try Managed Cloud to get up and running instantly with a fully turnkey, zero-config deployment.




Top comments (0)