Referrals
Prov0.1.0A referral program inside your app: personal referral links with click tracking, first-touch attribution on sign-up, conversion tracking and a reward ledger your team approves and pays out.
terminalnpx ajolla add referralsWhat you get
- Personal referral links with customisable vanity codes, copy and share buttons
- Click tracking at /r/[code]: bot filtering, one count per visitor per day, salted IP hashes only
- First-touch attribution cookie with a configurable window and a safe, same-origin landing page
- Sign-up attribution with self-referral, duplicate and existing-user protection
- Idempotent conversion tracking with fixed or percentage rewards for referrer and referee
- Reward ledger: pending → approved → paid or void, with fraud flags and CSV export
- Referrer dashboard with stats, masked referred users and an opt-in leaderboard
- Reward notifications via the Notifications module
Routes it adds
/admin/referrals/admin/referrals/rewards/admin/referrals/settings/r/[code]/referrals
/api/ajolla/referrals/export
Plus 1 Supabase migration with row-level security, and an AGENTS.md your coding agent reads after install.
Installs with
Kernel (added automatically).
Configuration
- REFERRALS_IP_SALT
- Random secret mixed into the IP hashes used to deduplicate clicks and flag fraud. Falls back to one derived from SUPABASE_SECRET_KEY.
- NEXT_PUBLIC_SITE_URL
- Absolute URL of your app (e.g. https://example.com); used for share links and notification links. Defaults to the request's host.
Events
Other modules (and your own code) can react to these:
referrals.clickedreferrals.signed_upreferrals.convertedreferrals.reward_approvednotifications.requested
Use it from any stack
Ajolla Hosted runs the same code as a multi-tenant API. Create an app in your dashboard, enable Referrals, and call /api/m/referrals/v1 with your app key.
OpenAPI document — feed it to Ajolla to generate SDKs in 7 languages and an MCP server.
AGENTS.md
Read the instructions installed with this module
markdown# Ajolla Referrals
A referral program inside your app. Every user gets a personal link (`/r/<code>`). Clicks are counted, the first link a visitor opens is remembered in a cookie, sign-ups are credited to the referrer, and purchases you record create rewards in a ledger that admins approve and pay out. "Admins" means users with the `referrals.manage` permission.
## What was installed
**Routes**
- `/referrals`: the signed-in user's referral home: their link with Copy and Share buttons, stats (visits, sign-ups, customers, rewards earned / pending / paid), the people they referred (emails masked: `j•••e@g•••l.com`), the leaderboard with an opt-in and display name, and a form to customise their code.
- `/r/[code]` (route handler): records the click, sets the attribution cookie and redirects to the landing page. `?to=/some/path` picks another page on your site; only same-origin paths are accepted, so it can't be used as an open redirect.
- `/admin/referrals`: program totals, top referrers, recent and flagged sign-ups.
- `/admin/referrals/rewards`: the ledger. Filter by status; approve, void or mark paid (with an optional note); export CSV.
- `/admin/referrals/settings`: program settings and click-data retention.
- `/api/ajolla/referrals/export[?status=…]`: the ledger as CSV (admins, max 10,000 rows).
**Components** (`components/ajolla/referrals/`)
- `<ReferralAttribution />`: put it once in your root layout. When a visitor who came through a referral link is signed in, it credits their referrer and clears the cookie. It does nothing (no request) unless a referral is waiting.
- `ShareLink`, the admin nav/badges in `admin.tsx`, and `referrals.module.css`.
**Code**
- `lib/ajolla/referrals/core.ts`: all logic, framework-free (also used by Ajolla Hosted).
- `lib/ajolla/referrals/shared.ts`: pure helpers safe for client code (code validation, cookie format, landing-path check, bot filter, email masking, money formatting).
- `lib/ajolla/referrals/server.ts`: `attributeReferral(user)`, `recordReferralConversion(userId, …)`, IP hashing and event emission.
- `lib/ajolla/referrals/csv.ts`, `lib/ajolla/admin/referrals.ts` (admin page registration).
**Tables** (`ajolla_referrals_`)
- `settings`: one row; defaults apply until an admin saves.
- `codes`: one per user (unique `user_id` and `code`). `click_count`, `signup_count`, `conversion_count` are kept by triggers; a guard trigger stops users from changing them or `last_ip_hash`.
- `clicks`: unique `(code_id, ip_hash, day)`: one row per visitor network per UTC day.
- `referrals`: unique `referred_user_id` (one referrer per user), `check (referrer_id <> referred_user_id)`, `fraud_flags` + `flagged`.
- `conversions`: unique `external_id`, so recording the same payment twice is a no-op.
- `rewards`: the ledger. Unique `reward_key` (`<referral>:first:<role>` or `<conversion>:<role>`) makes reward creation idempotent.
**RLS**
- **Users:** read and create their own code; change only its code, display name and opt-in. Read referrals, conversions and rewards they're part of. Read the program settings.
- **Server-side (secret key):** clicks, attribution, conversions and reward creation, which involve anonymous visitors or other users, are validated in code and written with the secret key. The leaderboard is read the same way and projected to opted-in names and counts.
- **Admins:** everything.
- **Anonymous visitors:** nothing.
**Events**
- `referrals.signed_up`, `referrals.converted`, `referrals.reward_approved`.
- `referrals.clicked`: off by default (one per unique visitor per day can be a lot). Turn it on in settings if you need it.
- `notifications.requested` when a reward is approved, to the person who earned it. Install Notifications to deliver it; without it nothing breaks.
**Env** (all optional)
- `REFERRALS_IP_SALT`: salts the IP hashes used for click dedupe and fraud checks. Raw IPs are never stored.
- `NEXT_PUBLIC_SITE_URL`: makes share links and notification links absolute (defaults to the request's host).
## Setup
1. Apply the migration: `supabase db push`.
2. Add `<ReferralAttribution />` to your root layout (`app/layout.tsx`):
```tsx
import { ReferralAttribution } from "@/components/ajolla/referrals/referral-attribution";
// inside <body>: <ReferralAttribution />
```
Or call `attributeReferral(user)` from `@/lib/ajolla/referrals/server` in your sign-up action or `/auth/callback` route, right after the account is created. Either way it's safe to call more than once.
3. Record purchases from your payment webhook (Stripe `invoice.paid`, `checkout.session.completed`, …):
```ts
import { recordReferralConversion } from "@/lib/ajolla/referrals/server";
await recordReferralConversion(userId, { externalId: invoice.id, amount: invoice.amount_paid, currency: invoice.currency });
```
`amount` is in minor units (cents). Users nobody referred are ignored, and repeating an `externalId` changes nothing.
4. Set up the program at `/admin/referrals/settings`, and link to `/referrals` from your navigation.
## How attribution works
- **Clicks:** `/r/<code>` ignores bots, link unfurlers and scripts (by user agent), and counts each visitor network once per code per day.
- **Cookie:** it sets an httpOnly cookie `ajolla_ref` (`<code>.<click time>`) for the attribution window (default 30 days), plus a readable `ajolla_ref_pending=1` flag. **First touch wins:** a later link doesn't replace an unexpired cookie.
- **Sign-up:** after sign-up the referral is credited, except when:
- the code belongs to the user themselves;
- the user already has a referrer;
- the account existed before the click (an existing user clicking a link isn't a referral).
- **Fraud signals:** a referral is flagged, not rejected, when:
- it signs up from the referrer's own network (the referrer's IP hash is refreshed when they open `/referrals`);
- it shares a network with another of the referrer's sign-ups that week;
- the referrer had 5 or more sign-ups in the past hour.
Flagged referrals' rewards always wait for manual review, even with auto-approve on.
## Rewards
- **Rules:**
- Fixed amount or percentage of the purchase, for the referrer and optionally the new customer.
- Paid on the first purchase only, or on every purchase.
- An optional cap per referrer (voided rewards don't count).
- **Ledger:** `pending → approved → paid`, or `void` from pending or approved. Paid and void are final, and concurrent admin changes are rejected.
- **Paying out** happens outside this module (PayPal, account credit, Stripe coupons…). Mark the reward paid, with a note such as the payout reference.
## Customising
- **Code rules and reserved words:** `RESERVED_CODES` and `codeProblem` in `shared.ts`.
- **Fraud thresholds:** `RAPID_SIGNUPS`, `REPEAT_IP_WINDOW_MS` and `CLICK_SLACK_MS` in `core.ts`.
- **Bot filter:** `BOT_UA` in `shared.ts`.
- **Click retention:** `CLICK_RETENTION_DAYS` in `core.ts`. Delete old raw clicks from the settings page; visit totals are kept.
- **Notification wording:** `rewardEvents` in `core.ts`.
- **Styles:** `components/ajolla/referrals/referrals.module.css`, plus the kit's `--aj-*` variables.