Skip to content

feat: referral system, auth rate limiting & bonus searches#3

Merged
creatornerd merged 1 commit into
mainfrom
feat/referral-system
May 27, 2026
Merged

feat: referral system, auth rate limiting & bonus searches#3
creatornerd merged 1 commit into
mainfrom
feat/referral-system

Conversation

@creatornerd
Copy link
Copy Markdown
Owner

Summary

  • Every logged-in user gets a unique referral link (?ref=CODE derived from their Supabase user ID)
  • Referral code stored in localStorage when a visitor lands with ?ref=, then claimed automatically on signup
  • Successful referral awards the referrer +10 bonus searches/week, stacking on top of the base limit
  • Authenticated users now have a 15 searches/week base limit (was unlimited); guests remain at 5
  • Self-referral and double-referral blocked server-side
  • New "Refer a Friend" panel in the user avatar menu showing link, copy button, and referral stats

New API endpoints

  • GET /api/referral/info — returns referral code, link, and stats for the logged-in user
  • POST /api/referral/claim — validates and processes a referral on signup
  • GET /api/referral/validate/:code — checks if a referral code is valid

Supabase migration required

Run the SQL in the PR description / conversation to create the referrals and bonus_searches tables before this goes live.

Test plan

  • Land on app with ?ref=VALIDCODE — confirm code stored in localStorage, URL cleaned
  • Sign up — confirm referral is claimed, referrer's bonus_searches incremented by 10
  • Sign in as referrer — open "Refer a Friend" from avatar menu, confirm stats show +10 bonus
  • Try self-referral — should be blocked with 400
  • Try referring same user twice — should be blocked with 400
  • Verify authenticated users hit 429 after 15 searches (not unlimited)
  • Verify bonus searches extend the limit correctly

🤖 Generated with Claude Code

- 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>
Copilot AI review requested due to automatic review settings May 27, 2026 00:22
@vercel
Copy link
Copy Markdown

vercel Bot commented May 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
findmypro Ready Ready Preview, Comment May 27, 2026 12:26am

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread server/index.js
Comment on lines +381 to +385
// 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
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread client/src/App.jsx
Comment on lines +842 to +844
}).finally(() => {
localStorage.removeItem(REFERRAL_KEY);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 by bonus_searches.bonus_count, in both api/index.js and server/index.js.
  • Adds client-side referral capture, auto-claim on auth, a ReferralPanel modal, and a "Refer a Friend" entry in the avatar menu.
  • Adds @supabase/supabase-js to server/package.json and 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.

Comment thread api/index.js
Comment on lines +325 to +328
const { data: allUsers } = await supabaseAdmin.auth.admin.listUsers();
const referrer = (allUsers?.users || []).find(
u => generateReferralCode(u.id) === referralCode
);
Comment thread api/index.js
}

function generateReferralCode(userId) {
return userId.replace(/-/g, '').slice(0, 8);
Comment thread api/index.js
Comment on lines +347 to +350
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() });
Comment thread api/index.js
.from('referrals')
.select('id')
.eq('referred_user_id', user.id)
.single();
Comment thread api/index.js

res.json({
referralCode,
referralLink: `https://findmyspecialist.vercel.app?ref=${referralCode}`,
Comment thread api/index.js
Comment on lines +29 to +36
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;
Comment thread client/src/App.jsx
Comment on lines +832 to +843
// 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);
Comment thread api/index.js
Comment on lines +341 to +343
const { error: insertErr } = await supabase
.from('referrals')
.insert({ referrer_id: referrer.id, referred_user_id: user.id, rewarded: true });
@creatornerd creatornerd merged commit 2775dac into main May 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants