SPEC 015 — Referrals
| Field | Value |
|---|---|
| Status | DRAFT |
| Priority | P2 — Growth |
| Backend | equa-server/modules/referral/ |
| Frontend | equa-web/src/modules/referrals/ |
| Endpoints | equa-server/modules/api/src/endpoints/referral-endpoints.ts |
1. Feature Purpose
The referral system incentivizes user growth through a multi-layered reward mechanism. Users generate unique referral links, invite friends via email, and earn EquaCash (platform virtual currency) through scratch card rewards. The system supports both personal and organization-level referrals, invitation tracking, EquaCash transfers between users/organizations, and transaction history. Google Contacts integration for bulk invites was previously supported but has been deprecated.2. Current State (Verified)
2.1 Referral Link Generation
| Detail | Value |
|---|---|
| File | equa-server/modules/referral/src/referral.ts |
| Function | randomString() → uniqueReferral() |
| Algorithm | crypto.randomBytes(6).toString('hex').slice(0, 6) — 6-character hex string |
| Uniqueness | Recursive retry if link already exists in DB |
| Auto-creation | Referral record created at registration and on first access to referral page |
2.2 Reward Generation
| Detail | Value |
|---|---|
| File | equa-server/modules/referral/src/referral.ts |
| Function | generateReward() |
| Distribution | Random 1–100 → $10 (50%), $25 (25%), $50 (20%), $100 (5%) |
| Reward types | RewardType.signup, RewardType.referral, RewardType.organizationInfo, RewardType.obcl |
| Scratch mechanic | Reward amount hidden until user “scratches” the card |
2.3 Referral Registration Flow
| Detail | Value |
|---|---|
| Function | handleReferralRequest() |
| Steps | 1. Generate referral link for new user 2. Insert signup reward 3. Add to waitlist 4. Look up referrer by link 5. Mark invitation as joined 6. Insert referral reward for referrer 7. Increment referrer’s count |
| IP limiting | REGISTRATION_IP_LIMIT env var (default 20 registrations per IP) |
| Blacklists | Email blacklist (getEmailBlacklist) and domain blacklist (getDomainBlacklist) |
2.4 Scratch Card Reveal
| Detail | Value |
|---|---|
| Function | revealCard() |
| Behavior | Accepts array of reward IDs; for each unscratched card owned by user, marks as scratched and adds reward to EquaCash balance |
| Organization rewards | If reward has an org association, cash goes to org’s balance |
| Idempotency | Already-scratched cards are returned unchanged |
2.5 EquaCash Transfer
| Detail | Value |
|---|---|
| Function | transferEquaCashFromRequest() |
| Validation | Sender must have sufficient balance (availableCash >= request.value) |
| Atomicity | Deducts from sender, adds to recipient, inserts transaction record |
| Request class | NewEquaCashRequest with from, to, fromType, toType, value, currency, currencyType, optional memo |
2.6 Transaction History
| Detail | Value |
|---|---|
| Function | transferHistoryFromRequest() |
| Fields per record | created, type, amount, totalAmount (running balance), earned (boolean), memo, source |
| Transfer types | EquaCashTransferType.earned, transferProfile, transferOrganization, spend |
| Source resolution | Earned from referral link, email, OBCL, profile, org, or annual subscription |
| Running balance | Computed in reverse chronological order |
2.7 Invitation System
| Detail | Value |
|---|---|
| Function | sendInvitationEmailFromRequest() |
| Input | Array of emails, referral link, recipient name |
| Template | authNotificationTemplates.userInvitation |
| Invitation URL | {appUrl}/invite/r?user={referralLink} (personal) or {appUrl}/invite/r?organization={orgLink}&user={userLink} (org) |
| Statuses | InviteStatus.invited, bounced, joined, registered |
| Upsert | Re-inviting same email updates existing invitation record |
2.8 Google Contacts (Deprecated)
| Detail | Value |
|---|---|
| Function | googleContacts() |
| Status | Throws BadRequest — “The legacy google contacts API is being removed by Google and no longer supported.” |
2.9 Company Info Collection
| Detail | Value |
|---|---|
| Function | addUserCompanyInfo() |
| Purpose | Collects company details during waitlist phase |
| Fields | name, email, phoneNumber, organizationType (public/private), scheduleTool (zoom/skype/meet), numberSecurityHolder |
| Reward | RewardType.organizationInfo scratch card on first submission |
Sends company info to info@equastart.io |
3. Data Model
Referrals
| Column | Type | Constraints |
|---|---|---|
| id | uuid | PK |
| user | uuid | FK to Users |
| referralLink | text | UNIQUE, 6-char hex |
| equaCash | numeric | DEFAULT 0, current balance |
| noOfReferrals | number | DEFAULT 0, count of successful referrals |
| status | UserStatus | Enum |
| ipAddress | text | Registration IP |
Rewards
| Column | Type | Constraints |
|---|---|---|
| id | uuid | PK |
| reward | number | Amount (25, 100) |
| type | RewardType | signup, referral, organizationInfo, obcl |
| scratched | boolean | DEFAULT false |
| scratchedDate | Date | Set when card is scratched |
| user | uuid | FK to Users (reward recipient) |
| organization | uuid | FK to Organizations (nullable) |
| recipientByLink | citext | Email of user who joined via link |
| recipientByEmail | citext | Email of user who joined via email invite |
Invitations
| Column | Type | Constraints |
|---|---|---|
| id | uuid | PK |
| citext | Invitee email | |
| user | uuid | FK to Users (inviter) |
| organization | uuid | FK to Organizations (nullable) |
| status | InviteStatus | invited, bounced, joined, registered |
| created | timestamp | Auto |
| modified | timestamp | Auto |
CompaniesInfo
| Column | Type | Constraints |
|---|---|---|
| id | uuid | PK |
| citext | Company contact email | |
| name | text | Company name |
| phoneNumber | text | |
| numberSecurityHolder | number | Expected member count |
| organizationType | OrganizationType | Enum (public/private) |
| scheduleTool | ToolType | Enum (zoom/skype/meet) |
| user | uuid | FK to Users, UNIQUE |
UserCoupons
| Column | Type | Constraints |
|---|---|---|
| user | uuid | FK to Users |
| code | text | Coupon code |
Transactions (EquaCash transfers)
| Column | Type | Constraints |
|---|---|---|
| id | uuid | PK |
| from | text | Sender address (user ID, org ID, or chargify) |
| fromType | AddressType | Enum |
| to | text | Recipient address |
| toType | AddressType | Enum |
| value | number | Transfer amount |
| currency | text | |
| currencyType | CurrencyType | Enum |
| memo | text | Optional note |
| created | timestamp | Auto |
4. API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/referral/:entity | Session | Get referral data (link, EquaCash balance, count, waitlist position) |
| GET | /api/v1/referral/:entity/rewards | Session | Get scratch cards with daily/monthly stats |
| GET | /api/v1/referral/:entity/invitations | Session | Get invitation list with stats |
| POST | /api/v1/referral/:entity/invite | Session | Send email invitations (array of emails) |
| POST | /api/v1/referral/:entity/reveal | Session | Scratch cards (array of reward IDs) |
| GET | /api/v1/referral/:entity/equacash | Session | Get EquaCash balance for org context |
| POST | /api/v1/referral/transfer | Session | Transfer EquaCash between users/orgs |
| GET | /api/v1/referral/:entity/history | Session | Get transaction history with running balance |
| POST | /api/v1/referral/company-info | Session | Submit company info during waitlist |
| GET | /api/v1/referral/waitlist-stats | Public | Get waitlist aggregate stats |
5. Frontend Components
Module: equa-web/src/modules/referrals/
Pages:
| Component | File | Purpose |
|---|---|---|
MyReferrals | pages/my-referrals.tsx | Personal referral dashboard with scratch cards |
OrganizationReferrals | pages/organization-referrals.tsx | Organization-level referral management |
TransferEquaCash | pages/transfer-equa-cash.tsx | Personal EquaCash transfer form |
TransferOrgEquaCash | pages/transfer-org-equa-cash.tsx | Organization EquaCash transfer form |
TransferHistoryPage | pages/transfer-history-page.tsx | Personal transaction history |
OrgTransferHistoryPage | pages/org-transfer-history-page.tsx | Organization transaction history |
| Component | File | Purpose |
|---|---|---|
MyReferralsShared | components/referral-component.tsx | Core referral UI (shared between personal and org views) |
ReferralStatistics | components/referral-statistics.tsx | Stats: total referrals, daily/monthly tickets, EquaCash earned |
ReferralInvitePanel | components/referral-invite-panel.tsx | Invite-a-friend panel with link sharing |
ReferralsTable | components/referrals-table.tsx | Table of sent invitations and their statuses |
ScratchCardList | components/scratch-card-list/scratch-card-list.tsx | Grid of scratch cards |
ScratchCard | components/scratch-card/scratch-card.tsx | Individual scratch card with reveal animation |
ScratchOffPanels | components/scratch-off-panels.tsx | Scratch card section container |
TransferCashForm | components/transfer-cash-form.tsx | EquaCash transfer form fields |
TransferInfo | components/transfer-info.tsx | Transfer confirmation display |
TransactionHistoryTable | components/transaction-history-table.tsx | Table of EquaCash transactions |
InviteForm | components/invite-form.tsx | Email invitation form |
EquaTransferComponent | components/equa-transfer-component.tsx | Transfer flow container |
ClaimEquaCash | components/claim-equacash/claim-equacash.tsx | EquaCash claim UI |
SnackBar | components/snack-bar.tsx | Toast notification for referral actions |
| Component | File | Purpose |
|---|---|---|
ConfirmTransferModal | components/modal/confirm-transfer-modal.tsx | Transfer confirmation dialog |
ScratchTicketModal | components/modal/scratch-ticket-modal/scratch-ticket-modal.tsx | Full-screen scratch card reveal |
LearnMoreModal | components/modal/learn-more-modal.tsx | Referral program explainer |
WaitlistModal | components/modal/waitlist-modal.tsx | Waitlist position display |
ConnectGmail | components/modal/connect-gmail.tsx | Gmail integration for contacts |
DisconnectGmail | components/modal/disconnect-gmail.tsx | Gmail disconnection flow |
SocialMediaLinks | components/modal/social-media-links.tsx | Social sharing options |
| Component | File | Purpose |
|---|---|---|
GoogleSignIn | components/google-sign-in/google-sign-in.tsx | Google OAuth for contact import (deprecated backend) |
ConnectToGoogle | components/connect-to-google/connect-to-google.tsx | Google connection UI |
6. Business Rules
- Referral link is a 6-character hex string, cryptographically random, guaranteed unique via recursive retry.
- Reward distribution uses weighted random: $10 (50%), $25 (25%), $50 (20%), $100 (5%).
- Signup reward: Every new user gets one scratch card at registration.
- Referral reward: The referrer gets one scratch card when their invitee registers.
- Organization info reward: User gets one scratch card for submitting company information.
- Scratch cards are one-time reveal — once scratched, the EquaCash is added to the user’s or org’s balance.
- IP rate limiting: Max
REGISTRATION_IP_LIMIT(default 20) referral registrations per IP address. - Email and domain blacklists prevent abusive registrations.
- EquaCash transfers require the sender to have a balance >= the transfer amount. Transfer is atomic: deduct, credit, record.
- Transaction history shows a running balance computed in reverse chronological order.
- Invitation upsert: Re-inviting the same email updates the existing record rather than creating a duplicate.
- Invitation statuses progress:
invited→registeredorjoined; bounced emails are tracked separately. - Google Contacts import is deprecated — backend throws
BadRequestif called. - Dual referral paths: Personal referral link (
?user={link}) and organization referral link (?organization={orgLink}&user={userLink}).
7. Acceptance Criteria
- New user receives a unique 6-character referral link at registration
- User can share referral link and track how many people registered through it
- Signup generates one scratch card for the new user
- Successful referral generates one scratch card for the referrer
- Scratch card reveal animation works and adds correct EquaCash to balance
- Reward amounts follow the weighted distribution (25/100)
- User can send email invitations to multiple addresses
- Invitation table shows status (invited, registered, joined, bounced) with daily/monthly stats
- EquaCash transfers work between users and between user/org
- Transfer fails gracefully when balance is insufficient
- Transaction history shows all transfers, earned rewards, and subscription spends with running balance
- IP rate limiting blocks excessive registrations from a single IP
- Email and domain blacklists prevent blocked addresses from registering
- Organization-level referral page shows org-specific rewards and invite tracking
8. Risks
| Risk | Impact | Mitigation |
|---|---|---|
EquaCash balance stored as numeric on referral record, updated non-atomically in some paths | Race condition on concurrent scratch/transfer | Wrap balance updates in database transactions with row-level locking |
| IP limit is per-IP, not per-session | Shared IPs (corporate, VPN) may hit limit prematurely | Consider adding session-based rate limiting alongside IP |
| Google Contacts integration deprecated but UI components remain | Confusing UI if buttons are still visible | Remove or hide Google Contacts UI components |
| Scratch card reward amounts are deterministic given the random seed | No guaranteed prize pool budget control | Implement budget caps or reward rate adjustment based on total EquaCash issued |
| Transaction history computed in application code | Performance degradation with many transactions | Add database-level running balance or pagination |
updateCash uses getUserByRewardId with user param that may actually be a reward ID | Potential data corruption | Refactor to use explicit parameter naming (noted as TODO in source) |