Skip to main content
RapidDev - Software Development Agency

How to Build a Affiliate Tracking App with Lovable

Build an affiliate tracking system in Lovable where referral links append a ?ref= parameter, clicks are deduplicated by IP hash to prevent inflation, a 30-day cookie window attributes conversions, and an Edge Function calculates commissions atomically — with a real-time affiliate dashboard showing clicks, conversions, and earned commissions.

What you'll build

  • Affiliate registration flow generating unique referral codes and trackable links
  • Click tracking with IP hash deduplication to prevent the same visitor from counting twice
  • 30-day cookie attribution window stored in localStorage and Supabase
  • Conversion and commission calculation Edge Function triggered on successful purchase
  • Affiliate dashboard with clicks, conversions, conversion rate, and earned/pending balance
  • Payout management page for admins showing pending balances and marking payouts complete
  • Performance charts showing clicks and conversions over time with Recharts
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate15 min read2–2.5 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build an affiliate tracking system in Lovable where referral links append a ?ref= parameter, clicks are deduplicated by IP hash to prevent inflation, a 30-day cookie window attributes conversions, and an Edge Function calculates commissions atomically — with a real-time affiliate dashboard showing clicks, conversions, and earned commissions.

What you're building

Affiliate tracking has two hard problems: attribution accuracy and fraud prevention. This build addresses both.

Click deduplication works by hashing the visitor's IP address plus User-Agent string. The hash is stored alongside the click record. Before inserting a new click, the Edge Function checks if a click with the same ip_hash and affiliate_id exists within the last 24 hours. If it does, the click is acknowledged but not counted. This prevents automated refresh attacks from inflating metrics without blocking legitimate repeat visitors who come back days later.

The 30-day attribution window uses a two-layer approach. When a visitor lands via a ?ref= link, the referral code is stored in both a Supabase click record and the visitor's localStorage. When a conversion event fires (checkout complete), the conversion Edge Function reads the stored ref code from the request body, looks up the most recent click from that affiliate within the last 30 days, and creates a commission record.

Commission records are created in 'pending' status. An admin can review and approve them, changing status to 'approved', which makes them eligible for payout. Paid commissions move to 'paid' status with a payout reference ID.

Final result

A complete affiliate tracking system with fraud-resistant click attribution, commission management, and a dashboard that gives affiliates full visibility into their performance.

Tech stack

LovableFrontend and affiliate dashboard
SupabaseDatabase with RLS
Supabase Edge FunctionsClick tracking and commission calculation (Deno)
shadcn/uiUI components
RechartsPerformance charts
Supabase AuthAffiliate authentication

Prerequisites

  • Lovable Pro account for Edge Function generation
  • Supabase project with SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in Cloud tab → Secrets
  • A working checkout or conversion event in your app that you can trigger the commission calculation from
  • Decision on commission rate structure (flat fee vs percentage, per product vs site-wide)
  • Optional: Stripe account for handling payouts via Stripe Connect

Build steps

1

Create the affiliate tracking schema

Ask Lovable to set up all the tables for affiliates, clicks, conversions, and commissions. The schema is designed to answer every performance question with simple indexed queries.

prompt.txt
1Create an affiliate tracking schema in Supabase.
2
3Tables:
4- affiliates: id, user_id (references auth.users, unique), code (text unique, generated 8-char alphanumeric), name, email, website_url, commission_rate (decimal, e.g. 0.10 for 10%), status ('pending' | 'active' | 'suspended'), payout_method (text), payout_details (jsonb, encrypted), created_at
5- affiliate_clicks: id, affiliate_id (references affiliates), ip_hash (text), user_agent_hash (text), landing_url (text), referrer_url (text), visitor_id (text, uuid stored in visitor's localStorage), created_at
6- affiliate_conversions: id, affiliate_id (references affiliates), click_id (references affiliate_clicks, nullable), order_id (text unique), order_amount (decimal), commission_amount (decimal), status ('pending' | 'approved' | 'paid'), payout_id (text), created_at
7- affiliate_payouts: id, affiliate_id (references affiliates), total_amount (decimal), commission_ids (uuid array), status ('processing' | 'completed' | 'failed'), payout_reference (text), processed_at, created_at
8
9RLS:
10- affiliates: users can SELECT and UPDATE their own row
11- affiliate_clicks and affiliate_conversions: affiliates SELECT their own rows (affiliate_id = sub-select of their affiliate row)
12- affiliate_payouts: affiliates SELECT their own
13- All INSERT via service role (Edge Functions only)
14
15Add indexes: affiliate_clicks(affiliate_id, created_at DESC), affiliate_clicks(ip_hash, affiliate_id, created_at), affiliate_conversions(affiliate_id, status), affiliate_conversions(order_id).

Pro tip: Hash the visitor's IP and User-Agent server-side in the Edge Function, never client-side. Receiving the hash from the client would allow fraud — the attacker could just change the hash. The Edge Function reads the real IP from the CF-Connecting-IP header and hashes it before storing.

Expected result: All tables are created with correct RLS. Indexes are in place. The affiliates table has a unique code column. TypeScript types are generated.

2

Build the click tracking Edge Function

Every visit via a ?ref= link calls this Edge Function. It deduplicates by IP hash and stores the click for later attribution. This must be fast — it runs on page load.

supabase/functions/track-click/index.ts
1// supabase/functions/track-click/index.ts
2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
4
5const corsHeaders = {
6 'Access-Control-Allow-Origin': '*',
7 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
8 'Content-Type': 'application/json',
9}
10
11async function hashString(input: string): Promise<string> {
12 const data = new TextEncoder().encode(input)
13 const hash = await crypto.subtle.digest('SHA-256', data)
14 return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16)
15}
16
17serve(async (req: Request) => {
18 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
19
20 try {
21 const supabase = createClient(
22 Deno.env.get('SUPABASE_URL') ?? '',
23 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
24 )
25
26 const { ref_code, landing_url, referrer_url, visitor_id } = await req.json()
27
28 if (!ref_code) return new Response(JSON.stringify({ tracked: false }), { headers: corsHeaders })
29
30 const { data: affiliate } = await supabase
31 .from('affiliates')
32 .select('id')
33 .eq('code', ref_code)
34 .eq('status', 'active')
35 .single()
36
37 if (!affiliate) return new Response(JSON.stringify({ tracked: false }), { headers: corsHeaders })
38
39 const ip = req.headers.get('CF-Connecting-IP') ?? req.headers.get('X-Forwarded-For') ?? 'unknown'
40 const ua = req.headers.get('User-Agent') ?? ''
41 const ip_hash = await hashString(ip + affiliate.id)
42 const user_agent_hash = await hashString(ua)
43
44 // Dedup: check for existing click from same IP for this affiliate in last 24h
45 const since = new Date(Date.now() - 86_400_000).toISOString()
46 const { count } = await supabase
47 .from('affiliate_clicks')
48 .select('*', { count: 'exact', head: true })
49 .eq('affiliate_id', affiliate.id)
50 .eq('ip_hash', ip_hash)
51 .gte('created_at', since)
52
53 if ((count ?? 0) > 0) {
54 return new Response(JSON.stringify({ tracked: false, reason: 'duplicate' }), { headers: corsHeaders })
55 }
56
57 const { data: click } = await supabase
58 .from('affiliate_clicks')
59 .insert({ affiliate_id: affiliate.id, ip_hash, user_agent_hash, landing_url, referrer_url, visitor_id })
60 .select('id')
61 .single()
62
63 return new Response(JSON.stringify({ tracked: true, click_id: click?.id }), { headers: corsHeaders })
64 } catch (err) {
65 return new Response(JSON.stringify({ error: 'Internal error' }), { status: 500, headers: corsHeaders })
66 }
67})

Pro tip: Include the affiliate_id in the IP hash (not just the raw IP). This means the same visitor can legitimately click links from two different affiliates within 24 hours — both clicks count, because the hashes differ.

Expected result: Calling the Edge Function with a valid ref_code creates a click record. Calling again from the same IP within 24 hours returns { tracked: false, reason: 'duplicate' }. The click record includes the correct affiliate_id.

3

Build the conversion and commission Edge Function

This fires when a purchase is completed. It looks up the attribution from the last 30 days, calculates commission, and creates a pending commission record.

prompt.txt
1Create a Supabase Edge Function at supabase/functions/track-conversion/index.ts.
2
3The function accepts POST with body: { order_id: string, order_amount: number, visitor_id: string }.
4
5Logic:
61. Check if a conversion for this order_id already exists in affiliate_conversions. If so, return { duplicate: true } to prevent double commission on retries.
72. Look up the most recent affiliate_clicks row WHERE visitor_id = p_visitor_id AND created_at > now() - interval '30 days', ordered by created_at DESC, LIMIT 1.
83. If no click found, return { attributed: false } organic sale, no commission.
94. Fetch the affiliate's commission_rate from the affiliates table.
105. Calculate commission_amount = order_amount * commission_rate, rounded to 2 decimal places.
116. INSERT into affiliate_conversions: affiliate_id, click_id, order_id, order_amount, commission_amount, status='pending'.
127. Return { attributed: true, affiliate_id, commission_amount }.
13
14This function is called from your existing checkout completion webhook or server action. It uses the visitor_id that the client stores in localStorage and sends at checkout.

Expected result: Completing a purchase with a visitor_id that has a click within 30 days creates a pending commission record. Duplicate order_ids are rejected. Purchases with no click history return attributed: false.

4

Build the affiliate dashboard

Affiliates need a clear view of their performance: their referral link, click stats, conversion rate, and earnings. Ask Lovable to build the dashboard page.

prompt.txt
1Build an affiliate dashboard at src/pages/AffiliateDashboard.tsx.
2
3Requirements:
4- Header Card: affiliate's unique referral link displayed with a Copy button. Show affiliate status Badge.
5- Four stat Cards: Total Clicks (30 days), Conversions (30 days), Conversion Rate (conversions/clicks as percentage), Pending Earnings (sum of commission_amount WHERE status='pending')
6- Recharts LineChart below stats: two lines, clicks and conversions per day for the last 30 days, with dual Y-axes. X-axis = date, formatted as 'Jan 15'.
7- Conversions DataTable: columns Date, Order Amount (currency), Commission (currency), Status Badge (pending=yellow, approved=green, paid=blue). Paginated, 20 rows.
8- A shareable link section: show the referral URL and allow the affiliate to add a custom UTM campaign parameter to it.
9
10All data fetches are scoped to the currently authenticated affiliate's rows via RLS. Use Promise.all for parallel fetches of stats.

Expected result: The dashboard shows real-time affiliate performance data. The referral link copy button works. The chart shows 30 days of data. The conversions table shows all commissions with correct status badges.

5

Build the admin payout management page

Admins need to review pending commissions, approve them, and process payouts. Ask Lovable to build the admin area.

prompt.txt
1Build an admin page at src/pages/AffiliateAdmin.tsx. Protect with role='admin' check.
2
3Two Tabs: 'Pending Approvals' and 'Affiliates'.
4
5Pending Approvals tab:
6- DataTable of all affiliate_conversions WHERE status='pending'
7- Columns: Affiliate Name, Order ID, Order Amount, Commission Amount, Date, Approve Button, Reject Button
8- Bulk select checkboxes. 'Approve Selected' Button updates status='approved' for all checked rows.
9- Filter by affiliate Select and date range.
10
11Affiliates tab:
12- DataTable of all affiliates with columns: Name, Email, Code, Commission Rate, Status Badge, Total Earned (sum approved+paid commissions), Pending Balance (sum pending commissions), Actions
13- 'Process Payout' Button per affiliate: opens a Dialog showing all approved commissions as line items, total payout amount, and a payout reference Input. Submitting inserts into affiliate_payouts and updates all included commission_ids status to 'paid'.
14- 'Edit Commission Rate' inline clicking the rate opens an editable Input that saves on blur.
15- Add Affiliate Button opens a Sheet with registration form.

Expected result: Admins can approve pending commissions individually or in bulk. The payout dialog shows correct totals. Processing a payout updates all selected commission statuses to paid and creates an affiliate_payouts record.

Complete code

src/hooks/useAffiliateTracking.ts
1import { useEffect } from 'react'
2import { supabase } from '@/integrations/supabase/client'
3
4const VISITOR_ID_KEY = 'aff_visitor_id'
5const REF_CODE_KEY = 'aff_ref_code'
6const REF_EXPIRY_KEY = 'aff_ref_expiry'
7const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000
8
9function getOrCreateVisitorId(): string {
10 let id = localStorage.getItem(VISITOR_ID_KEY)
11 if (!id) {
12 id = crypto.randomUUID()
13 localStorage.setItem(VISITOR_ID_KEY, id)
14 }
15 return id
16}
17
18export function useAffiliateTracking() {
19 useEffect(() => {
20 const params = new URLSearchParams(window.location.search)
21 const refCode = params.get('ref')
22
23 if (refCode) {
24 // Store attribution with 30-day expiry
25 localStorage.setItem(REF_CODE_KEY, refCode)
26 localStorage.setItem(REF_EXPIRY_KEY, String(Date.now() + THIRTY_DAYS_MS))
27 }
28
29 const storedRef = localStorage.getItem(REF_CODE_KEY)
30 const expiry = localStorage.getItem(REF_EXPIRY_KEY)
31 const isExpired = expiry && Date.now() > parseInt(expiry)
32
33 if (isExpired) {
34 localStorage.removeItem(REF_CODE_KEY)
35 localStorage.removeItem(REF_EXPIRY_KEY)
36 return
37 }
38
39 const activeRef = refCode ?? storedRef
40 if (!activeRef) return
41
42 const visitorId = getOrCreateVisitorId()
43
44 // Fire-and-forget click tracking
45 supabase.functions.invoke('track-click', {
46 body: {
47 ref_code: activeRef,
48 landing_url: window.location.href,
49 referrer_url: document.referrer,
50 visitor_id: visitorId,
51 },
52 }).catch(() => { /* non-critical, do not block the page */ })
53 }, [])
54
55 const getAttributionData = () => {
56 const refCode = localStorage.getItem(REF_CODE_KEY)
57 const expiry = localStorage.getItem(REF_EXPIRY_KEY)
58 if (!refCode || (expiry && Date.now() > parseInt(expiry))) return null
59 return {
60 ref_code: refCode,
61 visitor_id: getOrCreateVisitorId(),
62 }
63 }
64
65 return { getAttributionData }
66}

Customization ideas

Multi-tier affiliate commissions

Add a referred_by column to affiliates so affiliates can recruit sub-affiliates. When a sub-affiliate earns a commission, calculate a smaller percentage (e.g. 5%) for the parent affiliate and create two commission records. This is called a two-tier affiliate program and is common in SaaS businesses.

Coupon code tracking

Add a coupon_code column to affiliates. Allow affiliates to share a discount code instead of (or in addition to) a link. In the checkout flow, when a user applies a coupon code, look up the affiliated affiliate and create a commission record. This handles influencers who share codes in video content where link tracking is impossible.

Affiliate portal with resources

Add a resources section to the affiliate dashboard with approved marketing materials: banner images, email templates, social media copy, and brand guidelines. Store files in Supabase Storage with a public bucket. Affiliates download what they need without contacting you.

Automated payout via Stripe Connect

Add a Stripe Connect onboarding flow to the affiliate registration. Store the connected account ID. Replace the manual payout process with an Edge Function that calls Stripe's Transfer API to send funds directly to the affiliate's bank account when an admin clicks 'Process Payout'.

Conversion funnel analytics

Add a funnel view to the admin page: total unique visitors via affiliate links, percent that started checkout, percent that completed checkout. This data comes from combining click records with conversion records. Show per-affiliate funnel metrics to identify top performers.

Common pitfalls

Pitfall: Tracking the raw IP address instead of a hash

How to avoid: Hash the IP address (salted with the affiliate_id) using SHA-256 before storing. The hash is sufficient for deduplication without storing the raw personal data. This also means if the hash database is ever breached, IPs cannot be reconstructed.

Pitfall: Attributing conversions using only a query parameter, not a cookie window

How to avoid: Store the ref code and expiry in localStorage when the visitor first lands (the useAffiliateTracking hook does this). Read it from localStorage at checkout. This persists the attribution for the full 30-day window across tabs and page refreshes.

Pitfall: Creating commission records directly from client-side code

How to avoid: All commission creation must happen in a server-side Edge Function called from your checkout completion webhook or server action. The Edge Function verifies the order_id against your orders table to confirm the purchase is real before creating the commission.

Pitfall: Not handling the case where a visitor clicks multiple affiliate links

How to avoid: The track-conversion function uses 'last click wins': it takes the most recent click within 30 days. Document this attribution model clearly in your affiliate terms of service so affiliates understand the rules.

Best practices

  • Use 'last click wins' attribution as the default and document it in your affiliate agreement. It is the simplest model to explain and implement, and it is the industry standard.
  • Never store raw IP addresses. Hash them server-side in the Edge Function. Include the affiliate_id as a salt so the same visitor's hash differs per affiliate.
  • Process commissions through a pending → approved → paid workflow so you have time to reverse fraudulent conversions before they are paid out.
  • Add a minimum payout threshold (e.g. $50) to reduce payment processing overhead. Store this in the affiliates table as min_payout_amount.
  • Log every call to track-conversion regardless of attribution outcome. Log attributed=false cases too. This audit trail is invaluable for dispute resolution when affiliates claim they should have received credit for a sale.
  • Rate-limit the track-click Edge Function to prevent bots from generating artificial clicks. Return 429 if the same IP sends more than 10 click tracking requests per minute.
  • Send a welcome email to new affiliates with their referral link, commission rate, and a link to the affiliate portal. Use an Edge Function triggered by a Postgres trigger on the affiliates table INSERT to call Resend.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building an affiliate tracking system where conversions are attributed to the most recent affiliate click within 30 days using a visitor_id stored in localStorage. I have affiliate_clicks and affiliate_conversions tables in Supabase. Help me write the Deno Edge Function track-conversion that: 1) checks for duplicate order_id, 2) finds the most recent click for the visitor_id within 30 days, 3) calculates commission as order_amount times affiliate commission_rate, and 4) inserts a pending commission record. Include CORS headers and proper error handling.

Lovable Prompt

Add a performance leaderboard to the affiliate admin page. Show the top 10 affiliates by conversion count for the current month, formatted as a ranked DataTable with columns: Rank, Affiliate Name, Clicks, Conversions, Conversion Rate, Revenue Generated, Commissions Earned. Add a month selector to change the reporting period. Highlight the top 3 with gold/silver/bronze badge icons next to their rank number.

Build Prompt

In Supabase, write a SQL query for an affiliate performance report. It should return per affiliate: affiliate name, code, total_clicks (last 30 days), total_conversions (last 30 days), conversion_rate (conversions/clicks), total_revenue (sum order_amount for approved+paid conversions last 30 days), total_commission (sum commission_amount for approved+paid last 30 days). Join affiliates with affiliate_clicks and affiliate_conversions. Include affiliates with zero clicks (LEFT JOINs). Order by total_revenue DESC.

Frequently asked questions

What happens if a user clears their localStorage before converting?

The attribution is lost. This is a known limitation of localStorage-based tracking. For higher attribution accuracy, you can supplement with a server-side cookie (set via your domain's own server) that persists the ref code. However, this requires a backend that can set HTTP cookies, which goes beyond Lovable's Edge Functions alone. For most use cases, localStorage with a 30-day window has acceptable attribution rates.

Can an affiliate inflate their own click count by visiting their link repeatedly?

No. The 24-hour IP hash deduplication in the track-click Edge Function prevents this. Repeated visits from the same IP hash within 24 hours return tracked: false and no new click record is inserted. The hash includes the affiliate_id, so visiting your own link and other pages do not interfere with each other.

How do I integrate the conversion tracking with Stripe payments?

In your Stripe checkout.session.completed webhook handler, read the client_reference_id (set this to the visitor_id when creating the Checkout Session) and the amount_total. Call the track-conversion Edge Function with these values. The visitor_id links the purchase to the affiliate click record, completing the attribution chain.

What commission rate structure should I use?

Percentage commissions (e.g. 20% of sale value) are common for SaaS products and scale naturally with order value. Flat-fee commissions (e.g. $25 per signup) work better for free-trial-to-paid funnels where order amounts vary. You can support both by adding a commission_type ('percentage' | 'flat') column to the affiliates table and adjusting the calculation in the Edge Function accordingly.

How do I prevent the same user from being counted as both a click and a conversion from two different affiliate links?

The track-conversion function uses 'last click wins' — it attributes the sale to the most recent click from the visitor_id within 30 days. If the user clicked affiliate A three weeks ago and affiliate B yesterday, affiliate B gets the credit. Only one commission record is created per order_id (enforced by the unique constraint).

Can I track affiliate performance for non-purchase conversions like signups or free trials?

Yes. The track-conversion Edge Function does not care what type of conversion it is — it just records an order_id, amount, and visitor_id. For a free trial signup, pass order_amount=0 and use a flat commission. The affiliate still gets credit for the signup. You can add a conversion_type column to distinguish purchase vs signup conversions in your reports.

How do I handle chargebacks or refunds affecting affiliate commissions?

Add a webhook for charge.dispute.created or refund Stripe events. When received, find the affiliate_conversion with the matching order_id and update its status to 'reversed'. If the commission was already paid, insert a negative adjustment record in a new affiliate_adjustments table that reduces the next payout. Always communicate refund policies clearly in your affiliate agreement.

Is there help available to build a more complex affiliate program?

RapidDev builds Lovable apps with multi-tier commissions, Stripe Connect payouts, and custom attribution models. Reach out if your affiliate program requires features beyond this guide.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.