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:
| Cluster | Endpoints | What it does |
|---|---|---|
| Resolve | affiliate/resolve/:code, affiliate/track | Validate a code, count a click |
| Affiliate | affiliate/me, affiliate/join, affiliate/programs | Affiliate self-service |
| Reporting | affiliate/referrals, affiliate/earnings | Affiliate 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
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. Frombase-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; }trackAffiliateClickis 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;
trackingCookieAgeon the affiliate program in AppEngine should match. - 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 } }resolveAffiliateCodereturns 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
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/intentendpoint persistsaffiliateCodeon the draft order. AppEngine validates it — invalid codes are silently dropped, and the order proceeds without one. - 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
affiliateCodeis paid, an AppEngine workflow:- Looks up the affiliate's program
- Calculates the commission per the program rules
- Creates a
commissionrecord linked to the order - Updates the affiliate's
pendingEarnings - Fires
commission.createdfor downstream notifications
You don't write any of that — it's a built-in workflow. You just attach the code.
- 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 thejoinAffiliateProgramhelper — it takesname,program, optionalreferredBy. AppEngine creates an affiliate record linked to the customer. - 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
Pay out commissions
Payouts happen on a schedule defined in the program (weekly, monthly). AppEngine's payout workflow:
- On the schedule, sums each affiliate's approved commissions
- Above a configured minimum (e.g., $50), creates a payout record
- Calls Stripe Connect transfer (or PayPal Mass Pay, or manual) to move funds
- 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 onpay.mybrand.com), pass the code via querystring across the domain hop instead.
What's next
- Affiliate program admin — set up programs, manage commissions in the UI.
- Storefront checkout tutorial — where the code attaches to the order.
- Activity tracking — pipe affiliate-attributed sessions into your analytics.