Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions REFERRAL_FEATURES_ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
# Referral System — Feature Roadmap

This document lists candidate features for the referral system, split into two tiers. Everything already shipped is
marked ✅. Everything below is a candidate — decide which ones to build next.

---

## What's already live (summary)

| Feature | Notes |
| -------------------------------------------- | ------------------------------------------------------------------------ |
| ✅ First-touch attribution | localStorage, 90-day expiry |
| ✅ Click tracking | IP, user-agent, UTM, referer → D1 |
| ✅ WAE analytics | Click counts by country / code / day |
| ✅ Conversion tracking | pending → completed on signup |
| ✅ User-managed referral username | Set once, change limited by `REFERRAL_SYSTEM_USERNAME_CHANGE` |
| ✅ Auto-generate referral username on signup | Derived from email prefix with uniqueness suffix loop |
| ✅ Duplicate-click deduplication | KV `ref:dedup:{ip}:{code}`, 20-min TTL (`REFERRAL_DEDUP_WINDOW_MINUTES`) |
| ✅ CSV export | `GET /api/referrals/export?format=csv` + Download button in dashboard |
| ✅ `/r/{username}` vanity URL | HTML + OG meta + JS redirect — registered in `cloudflare-worker.ts` |
| ✅ Referral dashboard | Stats, activity feed, copy link, Download CSV |
| ✅ Admin tracking page | All-user conversion list |
| ✅ RESTful API | `/api/referrals/*` |

---

## Tier 1 — Simple, Good-to-Have (10 ideas)

These are self-contained, low-risk additions that fit naturally into the existing architecture. Each one can be built in
a single PR.

---

### 1. Auto-generate referral username on signup

**What:** When a new user registers and no referral username is set, automatically derive a username from their display
name or email prefix (`john.doe@` → `johndoe`) and save it.

**Why:** Users get a share-ready link immediately; zero friction.

**Where:** `processReferralAttribution` / Auth.js sign-in callback. New helper `generateReferralUsername(user)` in
`@ottabase/referrals/validation`. Add a uniqueness-suffix loop (`johndoe2`, `johndoe3` …) if taken.

---

### 2. Conversion rate display in the dashboard

**What:** Add a "Conversion rate" stat card next to Total / Conversions / Pending:

```
Conversion rate = completed / (completed + pending) × 100
```

**Why:** The most useful KPI for any referral programme — it's one arithmetic expression on data that's already returned
by `/api/referrals/user`.

**Where:** Pure UI change in `ReferralDashboard.tsx`. No schema or API change needed.

---

### 3. One-click social sharing buttons

**What:** Pre-formatted share URLs for Twitter/X, LinkedIn, and WhatsApp directly in the dashboard, next to the "Copy"
button.

```
Twitter: https://twitter.com/intent/tweet?text=Join+via+my+link:+{link}
LinkedIn: https://www.linkedin.com/shareArticle?url={link}
WhatsApp: https://wa.me/?text={link}
```

**Why:** Dramatically lowers the effort to share. No backend work; pure UI.

**Where:** `ReferralDashboard.tsx` — Referral Link card.

---

### 4. Referral source label in the activity feed

**What:** Parse the stored `referer` header into a human-readable label ("Twitter", "Facebook", "Reddit", "Direct",
"Other") and show it in the tracking table.

**Why:** Users want to know _where_ their clicks came from without decoding raw URLs.

**Where:** Pure display utility in `ReferralDashboard.tsx` / `ReferralTracking.getBrowserInfo()` style helper. No schema
change.

---

### 5. QR code for the referral link

**What:** A "Show QR Code" button in the Referral Link card that renders a QR code using the browser-native
`window.QRCode` API or a tiny canvas-based lib (e.g. `qrcode` npm, ~7 KB).

**Why:** Great for offline use, printed materials, and conference name-badges.

**Where:** `ReferralDashboard.tsx` — Referral Link card. Optional dep added only to the app, not shared packages.

---

### 6. Referred-by display on the user's own profile/settings

**What:** If `referredById` is set on the user, show a small "Referred by: @username" note on the user's settings or
profile page.

**Why:** Nice social acknowledgement; confirms the attribution is working.

**Where:** Add `GET /api/referrals/referrer` (returns `{ referralUsername }` of the referrer), then display in the
profile UI.

---

### 7. Referral milestone badges / in-app notifications

**What:** When a user crosses a referral count milestone (1st, 5th, 10th, 25th, 50th conversion), show a toast/banner in
the dashboard celebrating it.

**Why:** Gamification keeps top referrers engaged. Pure client-side calculation on data already loaded.

**Where:** `ReferralDashboard.tsx` — compute `milestoneMessage` from `stats.completed` on mount, pop a
`toast.success()`.

---

### 8. Duplicate-click deduplication (basic fraud prevention)

**What:** In `handleReferralTrack`, skip creating a new WAE event if the same IP has already fired for the same
`referralCode` within the last N minutes (tracked in KV with a TTL).

**Why:** Prevents a single user from inflating click counts by refreshing the page.

**Where:** `worker/routes/referrals.ts` — `handleReferralTrack`. Use `OBCF_KV` (already bound) with key
`ref_dedup:{ip}:{code}` and 15-min TTL. Config flag `REFERRAL_DEDUP_WINDOW_MINUTES` (default `15`, `0` = disabled).

---

### 9. Export referral data as CSV

**What:** A "Download CSV" button in the activity feed that calls `GET /api/referrals/export?format=csv` and downloads
the user's tracking records as a comma-separated file.

**Why:** Power users want their data. Requested feature in many SaaS products.

**Where:** New route handler `handleReferralExport` in `worker/routes/referrals.ts`. Generates CSV in memory from
`ReferralTracking.forUser(userId)`.

---

### 10. Referral link preview / custom `/r/{username}` vanity URL

**What:** Add a route `/r/:username` that redirects to `/?ref=:username` with a proper `302` and injects OG meta tags
(`og:title`, `og:description`, `og:image`) so link previews on social media show a personalised card rather than the
generic homepage preview.

**Why:** `?ref=` params look spammy; `/r/johndoe` is clean and memorable.

**Where:** New catch-all worker route `/r/:username` → read user record → redirect with meta-injected HTML (reuse the
existing `brand-html-inject` pattern).

---

## Tier 2 — High-Level / Larger Features (5 ideas)

These require more planning (schema changes, multi-step flows, or new packages) but would significantly elevate the
referral programme.

---

### A. Rewards & Incentives Engine

**Vision:** Define configurable rewards that are automatically granted when a referral converts — account credits,
coupon codes, feature unlocks, or custom callback webhooks. Both the referrer _and_ the new user can receive rewards
(double-sided referral).

**Key pieces:**

- `rewards` config table: `{ trigger: 'conversion', grantType: 'credit', amount: 10 }`
- `referral_rewards` table: `{ userId, trackingId, grantType, amount, status, grantedAt }`
- Queue job `referral.reward.grant` dispatched on conversion
- Dashboard: "You earned $10 credit" banner

---

### B. Multi-Tier / Chain Referrals

**Vision:** Support referral chains where A referred B who referred C, so A gets a partial reward for C's conversion
(configurable depth and split percentages).

**Key pieces:**

- `referralChain` JSON column on `referral_tracking`: `['userId-A', 'userId-B']`
- Attribution walker that climbs the chain up to `REFERRAL_MAX_DEPTH` levels
- Per-tier reward config: `[{ depth: 1, pct: 100 }, { depth: 2, pct: 20 }]`

---

### C. Campaign Management

**Vision:** Admins create named referral campaigns (e.g. "Black Friday 2025") with custom expiry dates, unique
campaign-scoped tracking URLs, per-campaign conversion goals, and campaign-specific reward overrides.

**Key pieces:**

- New `referral_campaigns` table: `{ id, name, startsAt, endsAt, goal, rewardConfig }`
- Campaign-scoped referral links: `/?ref=johndoe&campaign=blackfriday`
- Admin campaign CRUD page
- Dashboard: campaign selector + per-campaign stats

---

### D. Fraud Detection & Risk Scoring

**Vision:** Automatically flag suspicious referral activity with a risk score per tracking record — VPN/datacenter IP
detection, velocity checks (too many conversions from the same /24 subnet in 24 h), disposable email detection on the
referred user.

**Key pieces:**

- `riskScore` integer column on `referral_tracking` (0–100)
- `status: 'suspicious'` in addition to existing `pending/completed/invalid`
- Background queue job `referral.risk.score` runs after each conversion
- Admin UI: filter by status=suspicious, one-click approve/invalidate

---

### E. White-Label Public Invite Page (`/invite/{username}`)

**Vision:** A fully branded, publicly accessible landing page at `/invite/{username}` that shows the inviter's name,
avatar, a personalised headline ("John Doe invites you to join!"), and a sign-up CTA — all themed with the app's brand
engine. Ideal for email campaigns and direct links.

**Key pieces:**

- Worker SSR route `/invite/:username` → fetches user record → renders branded HTML
- Extend `brand-html-inject` to accept per-page OG meta overrides
- Optional: `referralBio` text field on the User model for a custom tagline
- Optional: integration with `@ottabase/ui-shadcn` for the client-side component after hydration

---

## Decision Matrix

| # | Feature | Effort | Impact | Dependencies | Status |
| --- | -------------------------- | -------- | --------- | --------------------- | ------- |
| 1 | Auto-generate username | Low | High | None | ✅ Done |
| 2 | Conversion rate display | Very Low | Medium | None | |
| 3 | Social sharing buttons | Very Low | High | None | |
| 4 | Source label | Very Low | Medium | None | |
| 5 | QR code | Low | Medium | Small npm dep | |
| 6 | Referred-by on profile | Low | Low | New API endpoint | |
| 7 | Milestone badges | Very Low | Medium | None | |
| 8 | Dedup / fraud prevention | Low | High | KV (already bound) | ✅ Done |
| 9 | CSV export | Low | Medium | None | ✅ Done |
| 10 | `/r/{username}` vanity URL | Medium | High | Worker route | ✅ Done |
| A | Rewards engine | High | Very High | Schema + Queue | |
| B | Multi-tier referrals | High | High | Schema changes | |
| C | Campaign management | High | High | New tables + Admin UI | |
| D | Fraud detection | Medium | High | Queue + scoring logic | |
| E | White-label invite page | Medium | High | Worker SSR | |
47 changes: 36 additions & 11 deletions REFERRAL_SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Added to `packages/ottaorm/src/models/User.ts`:
{
referralUsername: text("referral_username").unique(),
referredById: text("referred_by_id"),
referralUsernameChanges: integer("referral_username_changes").default(0).notNull(),
}
```

Expand Down Expand Up @@ -216,6 +217,8 @@ Response: 200
- Letters, numbers, underscores only
- Must be unique
- Returns 400 with error if validation fails
- Returns 400 with `USERNAME_CHANGE_LIMIT_REACHED` code if the user has already changed their username the maximum
number of times (configurable via `REFERRAL_SYSTEM_USERNAME_CHANGE` env var, default: 1)

### Register with Referral Attribution

Expand Down Expand Up @@ -309,6 +312,16 @@ features: {
- **Behavior:** Expired codes are automatically cleared from localStorage
- **Common values:** 30, 60, 90, 180, 365

### Environment Variables

#### `REFERRAL_SYSTEM_USERNAME_CHANGE` (default: `1`)

- **Type:** `string` (parsed as integer)
- **Description:** How many times a user can change their referral username **after initial setup**
- **Default:** `"1"` — users may set the username once and change it one more time
- **`"0"`** — username is locked after initial setup (no changes allowed)
- **Set in:** `wrangler.jsonc` `vars` section or as a Worker secret

### Example Configurations

**Minimal tracking (conversions only):**
Expand Down Expand Up @@ -547,9 +560,9 @@ Referral usernames must follow these rules (enforced in `@ottabase/referrals/val
Example validation:

```typescript
import { validateReferralUsername } from '@ottabase/referrals';
import { validateUsername } from '@ottabase/utils/user';

const result = validateReferralUsername('john_doe123');
const result = validateUsername('john_doe123');
if (!result.valid) {
console.error(result.error);
}
Expand Down Expand Up @@ -604,6 +617,7 @@ When a user changes their referral username:
- Pending referrals with old code may not convert
- A warning is shown in the UI
- Completed conversions remain linked
- **Change limit is enforced** (configurable via `REFERRAL_SYSTEM_USERNAME_CHANGE` env var, default: 1)

## Testing Checklist

Expand Down Expand Up @@ -700,15 +714,26 @@ When a user changes their referral username:

## Future Enhancements

- [ ] Email notifications for conversions
- [ ] Reward/incentive system
- [ ] Admin analytics dashboard
- [ ] Referral leaderboard
- [ ] Custom referral link URLs (e.g., `/r/{username}`)
- [ ] Multi-level referrals (referral of referral)
- [ ] Export referral data (CSV/JSON)
- [ ] Webhook notifications for conversions
- [ ] A/B testing for referral campaigns
See **[REFERRAL_FEATURES_ROADMAP.md](./REFERRAL_FEATURES_ROADMAP.md)** for a full list of candidate features, split
into:

- **Tier 1 — Simple, good-to-have** (10 ideas, each buildable in a single PR)
- **Tier 2 — High-level / larger features** (5 strategic ideas)

Quick reference of items not yet started:

| Tier 1 (simple) | Tier 2 (high-level) |
| ----------------------------------------- | ------------------------------------- |
| Auto-generate referral username on signup | Rewards & incentives engine |
| Conversion rate stat in dashboard | Multi-tier / chain referrals |
| One-click social sharing buttons | Campaign management |
| Source label in activity feed | Fraud detection & risk scoring |
| QR code for referral link | White-label `/invite/{username}` page |
| Referred-by on user profile | |
| Milestone badges / in-app notifications | |
| Duplicate-click deduplication | |
| CSV export | |
| `/r/{username}` vanity URL | |

## License

Expand Down
9 changes: 9 additions & 0 deletions apps/ottabase-template-app-tanstack/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,12 @@ KILLSWITCH_LOCKDOWN=false
# By default destructive migrations are disabled. Set to '1' or 'true' to enable.
MIGRATION_ALLOW_DESTRUCTIVE=0

# Referral system: number of times a user can change their referral username after initial setup.
# Set to '0' to disallow any changes after first set. Default is 1.
REFERRAL_SYSTEM_USERNAME_CHANGE=1

# Referral click deduplication window (minutes). Within this window a second click from the
# same IP+referral-code pair is silently ignored (not counted in analytics).
# Set to '0' to disable deduplication. Default is 20.
REFERRAL_DEDUP_WINDOW_MINUTES=20

11 changes: 11 additions & 0 deletions apps/ottabase-template-app-tanstack/cloudflare-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { handleBootstrapRoute, interceptIfNotReady, resolvePlatformState } from
import { injectBrandCriticalCSS } from './worker/lib/brand-html-inject';
import { initDbConnection } from './worker/lib/db-utils';
import { checkKillSwitches } from './worker/lib/killswitch';
import { handleReferralVanityRedirect } from './worker/routes/referrals';
import { resolveApiRoute } from './worker/routes/router';
import { handleShortlinkFallback } from './worker/routes/shortlinks';

Expand Down Expand Up @@ -118,6 +119,16 @@ export default {
return shortlinkFallbackResponse;
}

// /r/{username} vanity referral redirect
const vanityMatch = normalizedPathname.match(/^\/r\/([^/]+)$/);
if (vanityMatch) {
const vanityRes = await handleReferralVanityRedirect(
{ request, env, url },
decodeURIComponent(vanityMatch[1]),
);
if (vanityRes) return vanityRes;
}

if (!env.OBCF_ASSETS) {
return errorResponse('Assets binding not configured', 500, {
code: 'CONFIG_ERROR',
Expand Down
Loading