Documentation

Parrainage

Système de parrainage avec code unique, tracking et hooks pour récompenses.

Chaque utilisateur reçoit automatiquement un code de parrainage unique (8 caractères alphanumériques lisibles, sans 0/O/1/I/L) à la création de son compte. Il peut le partager via un lien https://tonsite.com/r/{code} — quand quelqu'un clique, on pose un cookie ref (httpOnly, 30j) puis on redirige vers /register. À l'inscription, le nouveau user est lié au parrain via User.referredById.

Architecture

FichierRôle
prisma/schema.prismaChamps referralCode (unique) et referredById (self-relation) sur User
lib/referral.tsGénération du code + cookie name/TTL
app/r/[code]/route.tsLanding page : valide le code, pose le cookie, redirige vers /register
app/api/auth/register/route.tsLit le cookie au signup credentials et fill referredById
lib/auth.tsevents.createUser fait pareil pour les signups OAuth
app/api/user/referrals/route.tsAPI : code + count + liste filleuls
app/[locale]/(account)/account/referrals/page.tsxUI : code, lien, partage, stats

Activer les récompenses

Le boilerplate trace qui parraine qui mais ne distribue rien. C'est à toi de brancher la logique de récompense en fonction de ton modèle.

1. Choisir le déclencheur

DéclencheurQuandAvantagesRisques
Inscriptionregister/route.tsImmédiatFraud massive (faux comptes)
Email vérifiéverify-email/route.tsFiltre les bots simplesReste fraude-able
Premier paiementstripe/webhook (customer.subscription.created)Seulement de "vrais" users — recommandé pour SaaSRécompense différée
Activité (X jours / N actions)Custom triggerFiltre les comptes mortsPlus complexe

2. Choisir la forme de la récompense

// Option A : crédit en cents sur User (appliqué à la prochaine facture)
model User {
  // ...
  creditCents Int @default(0)
}

// Option B : table dédiée (audit + historique)
model ReferralReward {
  id         String   @id @default(cuid())
  userId     String   // bénéficiaire (parrain ou filleul)
  referredId String?  // l'user qui a déclenché la récompense
  kind       String   // "credit", "coupon", "free_month"
  amount     Int      // cents ou jours
  redeemedAt DateTime?
  createdAt  DateTime @default(now())
  user       User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

// Option C : coupon Stripe
// → créer un coupon via Stripe API et stocker l'ID sur User

3. Exemple : reward sur premier paiement

// app/api/stripe/webhook/route.ts
case "customer.subscription.created": {
  const subscription = event.data.object as Stripe.Subscription;
  const user = await prisma.user.findFirst({
    where: { subscription: { stripeCustomerId: subscription.customer as string } },
    select: { id: true, referredById: true },
  });

  if (user?.referredById) {
    // Anti-self-referral
    if (user.referredById === user.id) break;

    // Récompense le parrain : 10€ de crédit
    await prisma.user.update({
      where: { id: user.referredById },
      data: { creditCents: { increment: 1000 } },
    });

    // Optionnel : récompense le filleul aussi (-5€ sur prochaine facture)
    await prisma.user.update({
      where: { id: user.id },
      data: { creditCents: { increment: 500 } },
    });

    // Log audit + notif
    await logAudit({ userId: user.referredById, action: "referral.rewarded" });
    await createNotification(user.referredById, {
      title: "Tu as gagné 10€ !",
      message: "Quelqu'un que tu as parrainé vient de souscrire.",
    });
  }
  break;
}

Anti-fraude

À garder en tête quand tu implémentes les récompenses :

  • Auto-parrainage : user.referredById !== user.id
  • Email vérifié obligatoire avant de récompenser
  • Rate-limit signup par IP (Upstash Redis dans lib/rate-limit.ts)
  • Détection de patterns : 50 inscriptions depuis la même IP en 1h → flag manuel
  • Capping : limite le nombre de filleuls qui rapportent par mois

Désactiver le parrainage

Si tu n'utilises pas la feature, retire simplement le lien dans la nav (components/layout/user-shell.tsx) et la page /account/referrals. Les champs referralCode et referredById restent en DB mais n'embêtent personne.

On this page