Documentation

Track affiliate conversions

Capture affiliate codes from URL parameters, attribute orders to affiliates, and pay out commissions — the base-app pattern.

Affiliate tracking is two pieces of work: pinning a code to the visitor when they arrive from an affiliate link, and attaching that code to whatever conversion they eventually do. AppEngine handles the affiliate program (registration, commission rates, payouts) — your Next.js app handles the cookie. This walkthrough plunders base-app/src/lib/affiliate-tracking.ts and affiliate-api.ts.

The flow

visitor arrives via /?ref=ALICE100
    → save 'ALICE100' to cookie
visitor browses, eventually checks out
    → cookie still set; checkout reads it; sends to AppEngine
AppEngine creates order with affiliateCode field
    → workflow on order.paid creates commission record for Alice
Alice's affiliate dashboard shows pending commission

Three AppEngine endpoint clusters carry it:

ClusterEndpointsWhat it does
Resolveaffiliate/resolve/:code, affiliate/trackValidate a code, count a click
Affiliateaffiliate/me, affiliate/join, affiliate/programsAffiliate self-service
Reportingaffiliate/referrals, affiliate/earningsAffiliate dashboard data

Prerequisites

  • AppEngine instance with the affiliate module enabled (admin → Settings → Modules → Affiliate)
  • At least one published affiliate program (admin → Affiliate → Programs)
  • Working storefront checkout, since affiliate codes attach at the order

Step-by-step

  1. 1

    Capture the code from the URL

    The visitor lands at /?ref=ALICE100 (or ?affiliate=..., ?aff=... — pick one and stay consistent). A client component saves it to a cookie. From base-app/src/lib/affiliate-tracking.ts:

    // src/lib/affiliate-tracking.ts
    const COOKIE_NAME = 'affiliateCode';
    
    export function saveAffiliateCode(code: string, days = 30) {
      if (typeof document === 'undefined' || !code) return;
      const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
      document.cookie = `${COOKIE_NAME}=${encodeURIComponent(code)};expires=${expires};path=/;SameSite=Lax`;
    }
    
    export function getAffiliateCode(): string | null {
      if (typeof document === 'undefined') return null;
      const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]*)`));
      return match ? decodeURIComponent(match[1]) : null;
    }
    
    export function clearAffiliateCode() {
      if (typeof document === 'undefined') return;
      document.cookie = `${COOKIE_NAME}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;SameSite=Lax`;
    }
    

    Mount a tracker component in the root layout:

    // src/components/AffiliateTracker.tsx
    'use client';
    import { useEffect } from 'react';
    import { useSearchParams } from 'next/navigation';
    import { saveAffiliateCode } from '@/lib/affiliate-tracking';
    import { affiliateAPI } from '@/lib/affiliate-api';
    
    export function AffiliateTracker() {
      const search = useSearchParams();
    
      useEffect(() => {
        const code = search.get('ref') ?? search.get('aff') ?? search.get('affiliate');
        if (!code) return;
        saveAffiliateCode(code, 30);
        affiliateAPI.trackAffiliateClick(code, 'link');
      }, [search]);
    
      return null;
    }
    

    trackAffiliateClick is fire-and-forget — it counts a click for reporting and ignores failures. Don't await it.

    A 30-day window is the most common setting; trackingCookieAge on the affiliate program in AppEngine should match.

  2. 2

    Validate the code (optional)

    If you want to show "You'll get 10% off — invited by Alice" on the landing page, resolve the code:

    const resolved = await affiliateAPI.resolveAffiliateCode(code);
    // { valid: true, affiliateName: 'Alice', programName: 'Friends', reward: { discountPercent: 10 } }
    

    resolveAffiliateCode returns null on invalid or expired codes. Show a default landing page in that case — don't reveal whether a code exists, since codes are sometimes treated as semi-private.

  3. 3

    Attach the code at checkout

    The cookie persists through the visitor's session. At checkout, read it and send it as part of the order create:

    // src/components/Checkout.tsx
    'use client';
    import { getAffiliateCode } from '@/lib/affiliate-tracking';
    import { storefrontAPI } from '@/lib/storefront-api';
    
    async function startCheckout(cart: Cart, customer: Customer) {
      const affiliateCode = getAffiliateCode();
      return storefrontAPI.createStripeIntent({
        cartId: cart.id,
        customer,
        affiliateCode: affiliateCode || undefined,
      });
    }
    

    The storefront/stripe/intent endpoint persists affiliateCode on the draft order. AppEngine validates it — invalid codes are silently dropped, and the order proceeds without one.

  4. 4

    Configure the commission rules

    In the admin UI, Affiliate → Programs. For each program:

    • Commission type — flat amount, percentage of order, or per-product
    • Commission rate — e.g., 15%
    • Window — same as the cookie window (30 days default)
    • Eligible products — all, or filtered by category/tag
    • Approval — auto, or requires admin review
    • Cookie key — defaults to affiliateCode, but pages a code is read from

    When an order with affiliateCode is paid, an AppEngine workflow:

    1. Looks up the affiliate's program
    2. Calculates the commission per the program rules
    3. Creates a commission record linked to the order
    4. Updates the affiliate's pendingEarnings
    5. Fires commission.created for downstream notifications

    You don't write any of that — it's a built-in workflow. You just attach the code.

  5. 5

    Build the affiliate dashboard

    Affiliates need a self-service page to see their referrals and earnings. The endpoints are already typed in affiliate-api.ts:

    // src/app/affiliate/page.tsx
    import { affiliateAPI } from '@/lib/affiliate-api';
    
    export default async function AffiliateDashboard() {
      const [profile, referrals, earnings] = await Promise.all([
        affiliateAPI.getAffiliateProfile(),
        affiliateAPI.getAffiliateReferrals(undefined, 1, 20),
        affiliateAPI.getAffiliateEarnings(1, 20),
      ]);
    
      return (
        <div className="mx-auto max-w-4xl py-8">
          <h1 className="text-2xl font-semibold">Affiliate dashboard</h1>
    
          <section className="mt-8 grid grid-cols-3 gap-4">
            <Stat label="Referrals" value={referrals.total} />
            <Stat label="Pending" value={`$${earnings.stats?.pending ?? 0}`} />
            <Stat label="Paid" value={`$${earnings.stats?.paid ?? 0}`} />
          </section>
    
          <h2 className="mt-12 text-lg font-medium">Recent referrals</h2>
          <table className="mt-2 w-full text-sm">
            <thead>
              <tr><th>When</th><th>Source</th><th>Status</th></tr>
            </thead>
            <tbody>
              {referrals.data?.map((r: any) => (
                <tr key={r.id}>
                  <td>{new Date(r.createdate).toLocaleDateString()}</td>
                  <td>{r.data.source}</td>
                  <td>{r.data.state}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      );
    }
    

    Affiliates self-register via affiliate/join. Build a registration form using the joinAffiliateProgram helper — it takes name, program, optional referredBy. AppEngine creates an affiliate record linked to the customer.

  6. 6

    Render an affiliate's referral link

    Each affiliate gets a unique code. Show their share-link prominently:

    // src/app/affiliate/share/page.tsx
    'use client';
    import { useState } from 'react';
    
    export default function SharePage({ code }: { code: string }) {
      const [copied, setCopied] = useState(false);
      const url = `${process.env.NEXT_PUBLIC_SITE_URL}/?ref=${code}`;
    
      return (
        <div>
          <input readOnly value={url} className="w-full rounded border p-2" />
          <button
            onClick={() => {
              navigator.clipboard.writeText(url);
              setCopied(true);
              setTimeout(() => setCopied(false), 2000);
            }}
          >
            {copied ? 'Copied' : 'Copy link'}
          </button>
        </div>
      );
    }
    

    Optionally generate a QR code (via qrcode.react) so affiliates can show codes in person.

  7. 7

    Pay out commissions

    Payouts happen on a schedule defined in the program (weekly, monthly). AppEngine's payout workflow:

    1. On the schedule, sums each affiliate's approved commissions
    2. Above a configured minimum (e.g., $50), creates a payout record
    3. Calls Stripe Connect transfer (or PayPal Mass Pay, or manual) to move funds
    4. Updates commission records to state: 'paid'

    Affiliate sees the payout in the dashboard. Admin sees it in the AppEngine finance module.

    For one-off manual payouts, in the admin: Affiliate → Affiliates → [name] → Pay out now. Useful when an affiliate disputes a calculation.

Edge cases

  • Two affiliate codes during the same session. Last write wins by default — the second link's code overwrites the first cookie. Some programs prefer first-touch; configure on the program.
  • Logged-in customer with own affiliate code. AppEngine ignores self-referrals — if the cookie's code resolves to the buying customer, no commission is created.
  • Refunds. A refunded order's commission flips to state: 'reversed'. If already paid, the affiliate's next payout is reduced by that amount (or, with high enough negative balance, AppEngine creates a reverse-transfer).
  • Cookie blocked. Some browsers (Safari ITP, brave with strict mode) drop third-party cookies aggressively. The cookie above is first-party, which survives. If you cross domains (affiliate landing on mybrand.com, checkout on pay.mybrand.com), pass the code via querystring across the domain hop instead.

What's next