CCFish's player referral system runs entirely on Cloudflare Workers + D1, tracking invite codes, reward claims, and conversion attribution with sub-50ms latency. In under 500 lines of TypeScript, we built a viral loop that turns every player into a growth channel — with zero server infrastructure and a monthly cost that fits in a cup of coffee.
The Problem
Indie mobile games face a brutal discovery problem. User acquisition costs have climbed 60%+ in three years, with CPI ranging from $0.50 for hyper-casual to $8 for mid-core titles. For CCFish — a casual card-fishing hybrid — we couldn't afford paid UA at scale. We needed organic growth, but asking players to "tell their friends" without structured incentives yields under 1% conversion. We needed a system that makes sharing frictionless, rewards both parties, tracks attribution reliably, and prevents abuse — all without a dedicated backend team.
The Solution
We built CCFish's referral system as a feature embedded directly into the game client with a serverless backend handling code generation, reward validation, duplicate detection, and conversion analytics.
Core Mechanics
**Shareable Invite Codes:** Every player gets a unique 6-character code generated from a seeded hash of their player ID. Sharing is one tap — native share sheet on iOS, system intent on Android — with a pre-filled message.
**Tiered Rewards:** Milestones drive quality referrals:
| Tier | Requirement | Referrer Reward | Referee Reward |
|------|-------------|-----------------|----------------|
| Bronze | Tutorial completion | 200 coins | 300 coins |
| Silver | 5 rounds played | 500 coins + 1 rare lure | 200 coins |
| Gold | 20 rounds played | 1500 coins + 1 rare fish | 100 coins |
| Platinum | First IAP | 3000 coins + exclusive skin | 20% bonus on first purchase |
This incentivizes referrers to help new players stay engaged.
**Double-Sided Incentive:** Both parties must feel they're winning. Single-sided rewards had abysmal conversion in early prototypes. Adding the referee bonus tripled invite conversions.
**Attribution:** Deterministic — when a new user enters an invite code during onboarding, we validate it, link the player IDs, then track downstream events for milestone-based reward crediting.
Architecture
The entire backend runs on Cloudflare's edge network:
| Component | Service | Role |
|-----------|---------|------|
| API Gateway | Cloudflare Workers | Request routing, CORS, auth |
| Database | Cloudflare D1 (SQLite) | Invite codes, rewards ledger, player links |
| Cache / Rate Limit | Cloudflare KV | Deduplication, rate limiting, idempotency |
| Analytics | Workers Analytics | Event tracking, funnel metrics |
Referral Code Generation
```typescript
async function handleGenerateCode(request: Request, env: Env): Promise<Response> {
const { playerId } = await request.json();
const rateKey = `rate:gen:${playerId}:${new Date().toISOString().slice(0,10)}`;
const attemptCount = await env.KV.get(rateKey, 'json') || 0;
if (attemptCount >= 3) {
return new Response(JSON.stringify({ error: 'rate_limit_exceeded' }), { status: 429 });
}
const code = generateCodeFromPlayerId(playerId);
await env.DB.prepare(`INSERT INTO referral_codes (player_id, code, status, created_at)
VALUES (?, ?, 'active', datetime('now'))
ON CONFLICT(player_id) DO UPDATE SET code = ?2, status = 'active'`)
.bind(playerId, code).run();
await env.KV.put(rateKey, String(attemptCount + 1), { expirationTtl: 86400 });
return new Response(JSON.stringify({ code, expiresIn: '7 days' }),
{ headers: { 'Content-Type': 'application/json' } });
}
```
Reward Claim & Idempotency
```typescript
async function handleClaimReward(request: Request, env: Env): Promise<Response> {
const { refereeId, milestone } = await request.json();
const link = await env.DB.prepare(`SELECT referrer_id FROM referral_links
WHERE referee_id = ? AND status = 'active'`).bind(refereeId).first();
if (!link) return new Response(JSON.stringify({ error: 'no_referral_link' }), { status: 404 });
const idempotencyKey = `claim:${refereeId}:${milestone}`;
if (await env.KV.get(idempotencyKey))
return new Response(JSON.stringify({ claimed: true }));
const rewardTable = {
'tutorial': { referrer: { coins: 200 }, referee: { coins: 300 } },
'rounds_5': { referrer: { coins: 500, lure: 1 }, referee: { coins: 200 } },
'rounds_20': { referrer: { coins: 1500, fish: 1 }, referee: { coins: 100 } },
'first_iap': { referrer: { coins: 3000, skin: 1 }, referee: { bonus: 0.2 } }
};
const reward = rewardTable[milestone];
if (!reward) return new Response(JSON.stringify({ error: 'invalid_milestone' }), { status: 400 });
await env.DB.batch([
env.DB.prepare(`UPDATE player_wallets SET coins = coins + ? WHERE player_id = ?`)
.bind(reward.referrer.coins, link.referrer_id),
env.DB.prepare(`UPDATE player_wallets SET coins = coins + ? WHERE player_id = ?`)
.bind(reward.referee.coins || 0, refereeId)
]);
await env.KV.put(idempotencyKey, 'true', { expirationTtl: 2592000 });
return new Response(JSON.stringify({ claimed: true, reward }));
}
```
D1 Schema
```sql
CREATE TABLE IF NOT EXISTS referral_codes (
player_id TEXT PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
status TEXT DEFAULT 'active',
created_at TEXT DEFAULT (datetime('now')),
expires_at TEXT DEFAULT (datetime('now', '+7 days'))
);
CREATE TABLE IF NOT EXISTS referral_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
referrer_id TEXT NOT NULL,
referee_id TEXT NOT NULL UNIQUE,
code_used TEXT NOT NULL,
reward_tier TEXT DEFAULT 'bronze',
status TEXT DEFAULT 'active',
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS reward_ledger (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_id TEXT NOT NULL,
reward_type TEXT NOT NULL,
amount INTEGER NOT NULL,
source TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
```
KV handles rate limiting (3 refreshes/day/player), claim idempotency, IP throttling (50/hr), and device fingerprint dedup to prevent self-referral loops.
Results
After 90 days live in CCFish v2.3:
| Metric | Before | After |
|--------|--------|-------|
| Organic installs/week | 1,200 | 4,800 |
| Effective CPI | $2.10 | $0.42 |
| D7 retention (referred) | — | 47% |
| D7 retention (organic) | 32% | 32% |
| Referral % of total installs | 0% | 38% |
| Viral K-factor | 0 | 0.32 |
38% of new installs came from referrals within 90 days, making it our top acquisition channel. Effective CPI dropped from $2.10 to $0.42 — the system paid for itself in under two weeks. Referred users retained at 47% D7 versus 32% for organic — the social commitment effect is real. The viral K-factor reached 0.32, meaning every 100 players generated 32 new players.
Key Takeaways
**1. Serverless is right for indie games.** Cloudflare Workers + D1 handled everything with zero ops burden — no servers, sub-10ms cold starts, global replication.
**2. Double-sided incentives are non-negotiable.** Early single-sided prototypes had abysmal conversion. Adding the referee bonus tripled invite-to-install rates.
**3. Tiered rewards drive quality.** Tying higher rewards to engagement milestones made referrers actively help onboard friends.
**4. Zero sharing friction is table stakes.** Native share sheets, deep link prefetching, pre-filled messages — every additional tap kills conversion.
**5. Measure the full funnel.** Track every step from code generation through milestone completion to find where your flow leaks.
**6. Build abuse resistance from day one.** Rate limiting, idempotency keys, and device fingerprinting must be in v1.
For indie developers stuck on the paid UA treadmill, embedding viral loops into game code on serverless infrastructure is proven and cheap. CCFish's system cost under $100 in Cloudflare bills and 40 hours of dev time — and it's now our primary growth engine.