product

Affiliate program

Built 2026-08-03. Beta testers become affiliate marketers who earn a recurring % commission on the Family Wall Plus subscriptions they refer. Generic by plan_key (launches on family_wall_plus); payouts are tracked here + paid manually (no Stripe Connect).

The loop

  1. Admin adds an affiliate at /dashboard/admin/affiliates — by account email, with a code (auto-derived from the email if blank) and a % (default 30). Admin-created ⇒ active immediately. Optional Comp Plus grandfathers them Family Wall Plus free (beta).
  2. The affiliate shares their link — /family-wall?ref=<code> — from their own dashboard at /dashboard/affiliate (link + share button + live stats + the FTC-disclosure reminder).
  3. A referred family is attributed first-touch: AcquisitionTracker captures ?ref=<code> and posts it to POST /api/affiliate/attribute, which write-once stamps profiles.referred_by_affiliate_code (ignores unknown/inactive codes, self-referral, and already-attributed profiles).
  4. Commission accrues on every payment. The invoice.payment_succeeded webhook (initial + each renewal) resolves the plan from the invoice price and the payer from our subscriptions row, then recordAffiliateCommission inserts one row into affiliate_commissions (idempotent per Stripe invoice — redelivery-safe). Amount = round(gross × commission_pct / 100).
  5. Admin pays out manually and clicks Mark paid — moves that affiliate's pending/approved commissions to paid.

Data (migration 621)

  • affiliates — one per user: code (unique), status (pending/active/disabled), commission_pct, is_beta, notes.
  • profiles.referred_by_affiliate_code — first-touch attribution (write-once).
  • affiliate_commissions — the ledger: one row per referred payment (gross_cents, amount_cents, status pending/approved/paid/reversed, stripe_invoice_id unique).

Service: affiliate.ts (AFFILIATE_PLANS allowlist — add a plan_key to extend). Routes: routes/affiliate.ts mounted /api/affiliate (POST /attribute, GET /me; super-admin GET /admin/list, POST /admin/create, PATCH /admin/:id, POST /admin/:id/mark-paid, POST /admin/:id/beta-comp).

Running it

  1. Apply migration 621 to prod.
  2. Redeploy the backend (new route + webhook branch).
  3. Confirm Stripe sends invoice.payment_succeeded to the webhook (it already handles it for coaching, so this is usually already on — check the Stripe dashboard webhook event list if commissions don't accrue on renewals).
  4. Add your beta testers as affiliates; Comp Plus so they can experience the paid tier; hand them their /dashboard/affiliate link.

Compliance — not optional, and it's the honest thing

  • FTC disclosure — BOTH sides. Affiliate-facing: affiliates must disclose they earn a commission when they share the link — the dashboard states this + gives a ready line. Referee-facing (added 2026-08-03, PH mesh catch): the person referred is also told — a dismissible ReferralDisclosure banner (components/ReferralDisclosure.tsx, mounted globally in layout.tsx) fires when a ?ref code is present: "you followed a friend's referral link; if you subscribe, they earn a small commission, never any extra cost to you." Affiliate-side and referee-side disclosure are separate requirements; disclosing to the affiliate does not disclose to the referee. Undisclosed affiliate promo is an FTC violation and off-brand.
  • Taxes / 1099. US affiliates paid ≥ $600 in a calendar year need a 1099-NEC, which means collecting a W-9 before you pay that much. The affiliate_commissions ledger (sum of paid per affiliate per year) is your record; collecting W-9s + issuing 1099s is a manual/owner step the system does not automate.
  • Program terms. Before scaling past a handful of trusted testers, a short affiliate agreement (what's allowed, no spam/paid-search-on-brand/cookie-stuffing, clawback on refunds/chargebacks, payout cadence) is worth having. Not built — a legal/owner task.
  • Clawbacks — wired. charge.refundedreverseCommissionForInvoice auto-reverses the matching commission (by Stripe invoice id) so a refunded sale stops counting toward a payout. Only unpaid rows (pending/approved) flip to reversed; if the commission was already paid out, it's left intact and logged with alreadyPaid: true — you can't silently un-pay the affiliate, so that one is a manual clawback (deduct from their next payout). Chargebacks/disputes (charge.dispute.created) are not yet auto-reversed — handle manually.

Founders window (Aug–Sep 2026) — auto-fulfillment

A time-boxed promo on /family-wall: sign up in the window and the founder perks are auto-granted, no admin step. grantFounderPerks(userId) (affiliate.ts) fires fire-and-forget after POST /api/family/wall (first wall creation): comps Family Wall Plus (grandfather-style) + creates an active founder affiliate (auto code from the email local-part, collision-suffixed, commission_pct 30, is_beta). Idempotent (skips whichever they already have) + no-op outside inFoundersWindow() (hard-coded 2026-08-012026-10-01). Founders find their link at /dashboard/affiliate. Remove/refresh after Sept 2026 (the date guard makes it inert automatically, but pull the /family-wall band + this path when the promo ends).

Generalizing to all HJ offerings (roadmap)

The engine is already generic (AFFILIATE_PLANS allowlist, recordAffiliateCommission({ planKey, grossCents, … })); attribution is global (AcquisitionTracker captures ?ref on any page). Extending affiliate programs to every major HJ offering is mostly wiring, in three tiers of effort:

  • Tier 1 — other subscription offerings — DONE (2026-08-03). AFFILIATE_PLANS now covers family_wall_plus, cornerstone, graphene_plus, write_cafe_pro, about_that_agent, about_that_brokerage (coaching excluded — it has its own coach split). They all accrue on the same invoice.payment_succeeded branch. AFFILIATE_OFFERINGS (label + path) drives the affiliate dashboard, which now shows a ref link per offering (Family Wall · Cornerstone · Graphene+ · write.cafe Pro · About That) — one code, promote whichever fits the audience. Per-offering commission % is still Tier 2 (today the rate is per-affiliate, flat across offerings).
  • Tier 2 — one-off purchases (real work). Persona Testing credit packs (persona_test_credit_purchases), Drift coins, etc. accrue via checkout.session.completed / a purchase-recording path, not invoice renewals. Generalize AFFILIATE_PLANS → an AFFILIATE_OFFERINGS map ({ key, revenueType: 'recurring' | 'oneoff', pct }) and add a recordAffiliateCommission call in each one-off purchase path (idempotent per checkout-session/purchase id).
  • Tier 3 — offering-aware surfaces. Per-offering earnings breakdown on /dashboard/affiliate, per-offering rate in admin, and an affiliate link that can target any offering's landing page (already works — ?ref is global). At this point an affiliate promotes the network, not one product.

The mesh version of this — one affiliate across HJ + QuickSites + PorchHearth — is a convention, not a shared service: crosstalk/contracts/affiliate-program.md. HJ's lean there is per-product first, adopt the shared-code + aggregated-owed convention only once a second product wants in.

Not built (deferred by design)

  • Stripe Connect automated payouts (chosen against — manual is right for a few testers).
  • Self-serve affiliate signup (invite-only in beta).
  • Auto-reverse on chargeback/dispute (refunds ARE auto-reversed); per-affiliate payout history view; W-9/1099 tooling.
AFFILIATE PROGRAM — Docs | HiveJournal