Referral Program — Technical Reference
Built from scratch (Fase D.7 / roadmap 4.15). Before this, the only trace of referrals was a stale @method processReferralBonus line in a JSDoc comment with no implementation behind it.
Where this lives
Backend
apps/backend/database/migrations/20260717100000-add-referral-program.js— addsusers.referral_code(unique) +users.referred_by_id(self-FK) and twosystem_settingcolumns for the bonus amounts (idempotent).apps/backend/database/models/user.js—referralCode,referredByIdattributes.SystemSetting.js—referralReferrerBonusCoins,referralReferredBonusCoins(both default 0).apps/backend/managers/user-managers/referral.manager.js—ensureReferralCode(lazy unique-code generation, unambiguous alphabet),getMyReferralInfo(code + referral count + total earned),applyReferralAtSignup(records the referrer once and grants both bonuses viacoinTransactionManager.createTransactionwith typereward/ relatedTypereferral; best-effort, idempotent). When the referrer's bonus is granted, it also sends them areferral_bonusnotification vianotificationManager.createNotification(notification.manager.js) —referral_bonusis aNotificationTypeenum value (notification.type.js) also accepted bynotification.validator.js. The new user does not get a notification for their welcome bonus.apps/backend/managers/user-managers/authentication.manager.js—registerpullsreferralCodeout of the input and callsapplyReferralAtSignupafter the account is created (best-effort — a referral failure never blocks registration).apps/backend/graphql/types/referral.type.js+resolvers/referral.resolver.js— theReferralInfotype, themyReferralInfoquery, andextend input UserRegistrationInput { referralCode }.apps/backend/graphql/types/admin/payout-admin.type.js+managers/admin-managers/payout-settings.manager.js— the two bonus amounts are exposed onPayoutSettings/PayoutSettingsInputand validated (non-negative integers) inupdateSettings.
Frontend
apps/frontend-nextjs/src/page-components/settings/ReferralPage.tsx(routed at/settings/referrals, linked from the settings menu) — queriesmyReferralInfo, shows the code + a copyable…/login?ref=CODElink + referral/earnings stats, plus one-tap share buttons (native share sheet, WhatsApp, Facebook, X).apps/frontend-nextjs/src/components/Login.tsx— reads?ref=CODEfrom the URL on mount and passesreferralCodein theRegistermutation input.apps/frontend-admin/src/app/payments/settings/page.tsx— two number inputs to set the referrer / new-user bonus amounts.
Technical implementation checklist
-
referralCode/referredByIdon users; unique code generated lazily -
myReferralInfoquery —ReferralPage.tsx -
referralCodeonUserRegistrationInput; captured from?ref=and applied at signup - Configurable bonuses on
SystemSetting, editable from the admin payments-settings page - Bonuses granted via the standard coin-transaction path (
reward/ relatedTypereferral), idempotent - Referrer notified (
referral_bonusnotification) when their bonus is granted - Affiliate commission on ongoing purchases — not built (one-time signup bonus only)
GraphQL API
# The caller's referral code + stats (code generated on first access)
query MyReferralInfo {
myReferralInfo { referralCode referralCount totalEarnedCoins }
}
# Registration input gained an optional referralCode
mutation Register($input: UserRegistrationInput!) {
register(input: $input) { token user { id username } }
}
# input: { username, email, password, dateOfBirth, referralCode: "ABCD2345" }
Design notes
- One-time signup bonus, not a running commission. The reward is granted once, when a referred user registers. A true affiliate commission (a cut of everything the referred user later buys) would need hooks into each purchase flow (coin purchases, subscriptions, shop, exclusive posts) and its own accounting — intentionally out of scope for this pass; the model records
referredByIdpermanently, so that data is available if commission is added later. - Off by default. Both bonus amounts default to 0, so no coins move until an admin sets them — the same "admin-configurable, default off" pattern as
verificationPriceCoins. - Idempotent & best-effort.
referredByIdis only ever set once (a second signup attempt with a code is a no-op), and the whole apply step is wrapped so a referral failure never blocks a successful registration. - Bonuses use the standard coin path.
coinTransactionManager.createTransaction({ transactionType: 'reward', relatedType: 'referral' })handles balance +UserCoinBalancebookkeeping, so referral earnings show up in the earnings history and count exactly like any other reward.