惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
有赞技术团队
有赞技术团队
宝玉的分享
宝玉的分享
雷峰网
雷峰网
Hugging Face - Blog
Hugging Face - Blog
V
V2EX
大猫的无限游戏
大猫的无限游戏
博客园 - 司徒正美
D
Docker
T
The Blog of Author Tim Ferriss
罗磊的独立博客
博客园 - 叶小钗
酷 壳 – CoolShell
酷 壳 – CoolShell
Blog — PlanetScale
Blog — PlanetScale
月光博客
月光博客
J
Java Code Geeks
Jina AI
Jina AI
博客园 - 【当耐特】
C
Check Point Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
腾讯CDC
Last Week in AI
Last Week in AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Visual Studio Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How to Build an Affiliate Program in Next.js (The Clean Way)
Zekariyas Be · 2026-05-28 · via DEV Community

You're shipping a Next.js SaaS. You want affiliates. You look at Rewardful — $49/month. FirstPromoter — $89/month. Impact — "contact sales." All of them to do one thing: track a ?ref= query param and attribute a Stripe payment to it.

That's it. That's the core problem. You're paying three figures a month for a cookie and a dashboard.

This guide shows you how to implement affiliate tracking yourself — the right way — and introduce a free, self-hosted alternative that handles the rest of the infrastructure you don't want to build.


The Core Logic of Affiliate Tracking in Next.js

Affiliate tracking boils down to three steps:

  1. Capture the ?ref= query parameter when a visitor lands
  2. Persist it in a cookie so it survives page navigation and checkout redirects
  3. Pass it to your payment processor (Stripe) at checkout time ### Capturing the Referral Param

In the App Router, you can't use useSearchParams directly in Server Components. You have two clean options:

Option A — Client Component with useSearchParams

Create a component that runs on the client and reads the URL:

// components/RefTracker.tsx
'use client';

import { useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import Cookies from 'js-cookie';

export function RefTracker() {
  const searchParams = useSearchParams();

  useEffect(() => {
    const ref = searchParams.get('ref');
    if (ref) {
      Cookies.set('affiliate_ref', ref, {
        expires: 30,       // 30-day window
        sameSite: 'lax',
        secure: process.env.NODE_ENV === 'production',
      });
    }
  }, [searchParams]);

  return null; // invisible component
}

Enter fullscreen mode Exit fullscreen mode

Drop this into your root layout (wrapped in <Suspense>):

// app/layout.tsx
import { Suspense } from 'react';
import { RefTracker } from '@/components/RefTracker';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Suspense fallback={null}>
          <RefTracker />
        </Suspense>
        {children}
      </body>
    </html>
  );
}

Enter fullscreen mode Exit fullscreen mode

Option B — Middleware (runs on every request, zero client JS)

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const ref = request.nextUrl.searchParams.get('ref');

  if (ref && !request.cookies.has('affiliate_ref')) {
    response.cookies.set('affiliate_ref', ref, {
      maxAge: 60 * 60 * 24 * 30, // 30 days
      httpOnly: true,
      sameSite: 'lax',
      secure: process.env.NODE_ENV === 'production',
    });
  }

  return response;
}

export const config = {
  matcher: ['/((?!_next|api|favicon.ico).*)'],
};

Enter fullscreen mode Exit fullscreen mode

Middleware is the better default — it's server-side, runs before the page renders, and doesn't require client JS. Use the Client Component approach only if you need the ref value accessible in browser JS.

Why a Cookie, Not localStorage?

  • Cookies are readable server-side — your API routes can access them during checkout
  • localStorage is origin-scoped but not path-scoped — fine for SPAs, fragile in server-rendered flows

- Cookies survive full-page reloads and redirects — Stripe checkout is a full redirect

Step-by-Step Code Implementation

Step 1 — Full Ref Capture + Cookie (TypeScript)

Here's a production-ready version using the middleware approach with validation:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

const REF_COOKIE = 'affiliate_ref';
const REF_MAX_AGE = 60 * 60 * 24 * 30; // 30 days

function isValidRef(ref: string): boolean {
  // Alphanumeric + hyphens only, 3–32 chars
  return /^[a-zA-Z0-9-]{3,32}$/.test(ref);
}

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const ref = request.nextUrl.searchParams.get('ref');

  if (ref && isValidRef(ref) && !request.cookies.has(REF_COOKIE)) {
    response.cookies.set(REF_COOKIE, ref, {
      maxAge: REF_MAX_AGE,
      httpOnly: true,
      sameSite: 'lax',
      secure: process.env.NODE_ENV === 'production',
      path: '/',
    });
  }

  return response;
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|api/).*)'],
};

Enter fullscreen mode Exit fullscreen mode

Step 2 — Pass the Ref to Stripe Checkout

In your checkout API route, read the cookie and attach it to the Stripe session as metadata:

// 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-04-10',
});

export async function POST(request: NextRequest) {
  const affiliateRef = request.cookies.get('affiliate_ref')?.value ?? null;

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [
      {
        price: process.env.STRIPE_PRICE_ID!,
        quantity: 1,
      },
    ],
    success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
    // Affiliate attribution lives here
    metadata: {
      affiliate_ref: affiliateRef ?? '',
    },
    subscription_data: affiliateRef
      ? {
          metadata: {
            affiliate_ref: affiliateRef,
          },
        }
      : undefined,
  });

  return NextResponse.json({ url: session.url });
}

Enter fullscreen mode Exit fullscreen mode

Key points:

  • metadata on the session is readable in your Stripe webhook
  • subscription_data.metadata ensures the ref persists on the subscription object for recurring commission tracking
  • A missing ref gets an empty string — never undefined, which would cause a Stripe API error ### Step 3 — Read the Ref in Your Stripe Webhook
// app/api/webhooks/stripe/route.ts (abbreviated)
import Stripe from 'stripe';

export async function POST(request: Request) {
  const event = stripe.webhooks.constructEvent(/* ... */);

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object as Stripe.Checkout.Session;
    const affiliateRef = session.metadata?.affiliate_ref;

    if (affiliateRef) {
      // Credit the affiliate — look up by ref slug, record the conversion
      await creditAffiliate(affiliateRef, session.amount_total ?? 0);
    }
  }
}

Enter fullscreen mode Exit fullscreen mode


The Infrastructure Problem: Self-Hosting vs. SaaS

Tracking the cookie is 50 lines of code. What you actually get from Rewardful for $49/month is everything else:

  • Affiliate dashboard — a portal where affiliates check their clicks, conversions, and earnings
  • Payout management — calculating commissions, generating invoices, sending payments
  • Multi-tier commissions — percentage vs. flat rate, recurring vs. one-time
  • Link management — custom slugs, campaign-level tracking Building all of that from scratch is weeks of work. You don't want to do it. You want to ship your actual product.

Enter RefearnApp

RefearnApp is a free, open-source, self-hosted affiliate tracking platform built specifically for Next.js + Stripe workflows.

What it gives you out of the box:

  • Affiliate dashboard — your affiliates get a real portal with their stats
  • Stripe-native integration — webhooks, metadata attribution, and payout tracking built in
  • Commission engine — configurable percentage or flat-rate commissions, recurring support
  • Zero monthly cost — deploy it on your own infra, keep 100% of your revenue
  • No vendor lock-in — it's your database, your data, your affiliate relationships It's the plug-and-play backend layer that sits between your cookie tracking code (above) and your affiliates. Instead of building creditAffiliate() yourself and wiring up a dashboard, you point your webhook at RefearnApp and it handles attribution, aggregation, and reporting.

Stack it's built for: Next.js (App Router), Stripe, PostgreSQL — the same stack you're already using.


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: