feat: referral system, auth rate limiting & bonus searches#3
Conversation
- Every logged-in user gets a unique referral link (?ref=CODE) - Referral code derived from first 8 chars of Supabase user ID - Storing ?ref= code in localStorage on landing, claimed on signup - Successful referral awards referrer +10 bonus searches/week - Self-referral and double-referral are blocked server-side - Authenticated users now have a 15 searches/week base limit - Bonus searches from referrals stack on top of the base limit - Added ReferralPanel UI accessible via the user avatar menu - Added /api/referral/info, /api/referral/claim, /api/referral/validate endpoints - Uses supabaseAdmin (service role key) for listUsers lookup - Added SUPABASE_SERVICE_ROLE_KEY to server and Vercel production env Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 89eebbf014
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Find the referrer by their code (first 8 chars of UUID without dashes) | ||
| const { data: allUsers } = await supabaseAdmin.auth.admin.listUsers(); | ||
| const referrer = (allUsers?.users || []).find( | ||
| u => generateReferralCode(u.id) === referralCode | ||
| ); |
There was a problem hiding this comment.
Use collision-resistant referral codes
The referral identity is derived from only the first 8 hex chars of a UUID and then resolved with .find(...), so two users who share that prefix will map to whichever match appears first. As the user base grows, prefix collisions become realistic, which can credit the wrong referrer or reject a valid claimant. Persist and resolve against a unique referral code (or a longer, uniqueness-enforced token) instead of a truncated UUID prefix.
Useful? React with 👍 / 👎.
| }).finally(() => { | ||
| localStorage.removeItem(REFERRAL_KEY); | ||
| }); |
There was a problem hiding this comment.
Preserve stored referral when claim request fails
The client deletes findmypro_referral in finally, so transient failures (network error, 5xx, or temporary auth issue) permanently drop the referral before it is successfully claimed. In that case the user has no retry path and the referral bonus is silently lost. Only clear the stored code after a confirmed successful claim response.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Introduces a referral program and tightens rate limiting. Logged-in users now have a 15-search/week cap (previously unlimited), and successful referrals add +10 bonus searches/week to the referrer. A new "Refer a Friend" panel surfaces the user's link and stats, and three new server endpoints back the flow. The client captures ?ref=CODE into localStorage and claims it on the next auth state change. Supabase tables referrals and bonus_searches are expected to be created via a migration (not present in this PR).
Changes:
- Adds
/api/referral/{info,claim,validate/:code}and a new auth rate-limit path (fmp:rl:user:<id>) extended bybonus_searches.bonus_count, in bothapi/index.jsandserver/index.js. - Adds client-side referral capture, auto-claim on auth, a
ReferralPanelmodal, and a "Refer a Friend" entry in the avatar menu. - Adds
@supabase/supabase-jstoserver/package.jsonand styles for the referral panel.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| api/index.js | Adds Supabase client, auth-aware rate limit with bonus, and 3 referral endpoints. |
| server/index.js | Mirrors the same referral and rate-limit logic for the local dev server. |
| client/src/App.jsx | Captures ?ref=, claims on auth, renders ReferralPanel, updates gate copy. |
| client/src/styles.css | Adds styling for the referral modal and stat blocks. |
| server/package.json | Adds @supabase/supabase-js dependency. |
| server/package-lock.json | Locks the new Supabase dependency tree. |
Files not reviewed (1)
- server/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const { data: allUsers } = await supabaseAdmin.auth.admin.listUsers(); | ||
| const referrer = (allUsers?.users || []).find( | ||
| u => generateReferralCode(u.id) === referralCode | ||
| ); |
| } | ||
|
|
||
| function generateReferralCode(userId) { | ||
| return userId.replace(/-/g, '').slice(0, 8); |
| const currentBonus = await getBonusSearches(referrer.id); | ||
| await supabase | ||
| .from('bonus_searches') | ||
| .upsert({ user_id: referrer.id, bonus_count: currentBonus + REFERRAL_BONUS, updated_at: new Date().toISOString() }); |
| .from('referrals') | ||
| .select('id') | ||
| .eq('referred_user_id', user.id) | ||
| .single(); |
|
|
||
| res.json({ | ||
| referralCode, | ||
| referralLink: `https://findmyspecialist.vercel.app?ref=${referralCode}`, |
| return count <= GUEST_WEEKLY_LIMIT; | ||
| } | ||
|
|
||
| async function checkAuthRateLimit(userId, bonusSearches = 0) { | ||
| const key = `fmp:rl:user:${userId}`; | ||
| const { result: count } = await upstash(`/incr/${key}`); | ||
| if (count === 1) await upstash(`/expire/${key}/604800`); | ||
| return count <= AUTH_WEEKLY_LIMIT + bonusSearches; |
| // Claim referral if one exists in localStorage | ||
| const storedRef = localStorage.getItem(REFERRAL_KEY); | ||
| if (storedRef) { | ||
| fetch(`${API_URL}/referral/claim`, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${session.access_token}`, | ||
| }, | ||
| body: JSON.stringify({ referralCode: storedRef }), | ||
| }).finally(() => { | ||
| localStorage.removeItem(REFERRAL_KEY); |
| const { error: insertErr } = await supabase | ||
| .from('referrals') | ||
| .insert({ referrer_id: referrer.id, referred_user_id: user.id, rewarded: true }); |
Summary
?ref=CODEderived from their Supabase user ID)localStoragewhen a visitor lands with?ref=, then claimed automatically on signupNew API endpoints
GET /api/referral/info— returns referral code, link, and stats for the logged-in userPOST /api/referral/claim— validates and processes a referral on signupGET /api/referral/validate/:code— checks if a referral code is validSupabase migration required
Run the SQL in the PR description / conversation to create the
referralsandbonus_searchestables before this goes live.Test plan
?ref=VALIDCODE— confirm code stored in localStorage, URL cleaned🤖 Generated with Claude Code