Each user can generate a personal referral code, share a short URL, and earn points/commission when their referees sign up and complete purchases. Codes can also be entered at sign-up time (Phase 1 attribution) — see Registration Flow.
Actors
Referrer — the user with the code.
Referred user — the new sign-up.
Backend — PointsService (backend/src/services/points/PointsService.ts), points routes at backend/src/routes/pointsRoutes.ts.
Sets user.referredBy = referrer._id on the new user.
Increments referrer.referralStats.totalReferrals.
Emits referral-signup to user-{referrerId} with the referee's name, email, and updated total.
At this point the referee is counted but no points have changed hands yet. Points award on subsequent business events.
4. Points awarding
PointsService.addPoints(userId, amount, source, metadata) (:36-100) is called by other services on triggering events:
Purchase completion (intended): when a referred user finishes an order, the referrer should get a commission. The hook point is PurchaseRequestServicenotifyTransactionCompleted — the exact wiring is implementation-specific; the service exposes source: 'purchase' | 'referral' | 'bonus' | 'admin'.
updateUserLevel(userId, session) recomputes the user's tier from LevelConfig.
Emits level-up on user-{userId} if the level changed (:91-99).
Both the referrer and the referee may earn points (e.g. "give 100, get 100" growth model). The current code awards per addPoints call — design decision lives in the caller, not in PointsService.
5. Redemption / payout
Users see their balance under /dashboard/account/points and can spend via POST /api/points/redeem (e.g. for service-credit or discount codes).
PointTransaction records type: 'spend' with negative amount, keeping balance running.
Sequence diagram
sequenceDiagram
autonumber
actor R as Referrer
actor N as New User
participant FE as Frontend
participant BE as Backend
participant DB as MongoDB
participant IO as Socket.IO
R->>FE: Generate referral code
FE->>BE: POST /api/points/generate-referral-code
BE->>DB: User.findByIdAndUpdate(referralCode=...)
BE-->>FE: { code }
R->>R: share https://amn.gg/r/{code}
N->>BE: GET /r/{code}
BE-->>N: 302 → /auth/jwt/sign-up?ref={code}
N->>FE: Fills sign-up, completes email verification
FE->>BE: POST /api/auth/verify-email-code (with referralCode in TempVerification)
BE->>DB: User.create
BE->>DB: referrer.referralStats.totalReferrals += 1
BE->>IO: emit user-{R} 'referral-signup'
Note over BE,DB: Later, when N completes a purchase
BE->>BE: PointsService.addPoints(R, +X, 'referral', {referredUserId:N})
BE->>DB: user.points += X; PointTransaction.create
BE->>BE: updateUserLevel → maybe 'level-up'
BE->>IO: emit user-{R} 'level-up'
API calls
Method
Endpoint
Purpose
POST
/api/points/generate-referral-code
Generate or rotate referral code
GET
/api/points/my-points
Balance + level
GET
/api/points/transactions
History
GET
/api/points/referrals
Referred users list
GET
/api/points/leaderboard
Global top referrers
GET
/api/points/levels
Level config (public)
POST
/api/points/redeem
Spend points
POST
/api/points/admin/add
Admin-only manual grant
GET
/r/:code
Short-URL redirect to sign-up
Database writes
users: referralCode on generation, referredBy on referee creation, referralStats.{totalReferrals, activeReferrals, totalEarned} and points.{total, available, level} on point events.
pointtransactions: one document per earn/spend/refund.
levelconfigs: read-only at runtime (seeded at deploy).
Socket events emitted
referral-signup → user-{referrerId} on referee creation.
level-up → user-{userId} when crossing a tier.
new-notification → standard notification channel for points-related milestones.
Side effects
The referee never sees the referrer's identity unless surfaced in UI.
points.available is the spendable balance; points.total is the lifetime accumulation (used for level tiers).
Transactions are wrapped in a Mongo session for atomicity (addPoints:47-88).
Error / edge cases
Code collision — extremely unlikely with 32^8 ≈ 1.1 × 10¹² combinations; the while-loop in generateReferralCode is a hard guarantee.
Self-referral — not blocked at controller level. Add a check if (referrer._id.equals(user._id)) return in verifyEmailWithCode and googleSignUp to prevent gaming.
Referral code entered with leading/trailing spaces — .trim() is applied (authController.ts:74, :127).
Referrer deleted — referredBy still points to the deleted user; the new user is effectively un-attributed. Soft-delete preservation is acceptable.
Points overflow — Number is sufficient up to 2⁵³; no overflow risk in practice.
Race on level-up — the Mongo session ensures user.points and PointTransaction are atomically updated, but two parallel addPoints calls might both trigger level-up emit. Idempotent in practice (frontend shows toast once).
activeReferrals — defined in referralStats but no code path increments it currently. Define "active" (e.g. referee has at least one completed purchase) and update accordingly.
[!tip] Track conversion, not just sign-ups
totalReferrals is incremented on sign-up; consider also tracking convertedReferrals (completed purchases) to measure real growth value.