Why Attribution Matters for Mobile Games

Every CCFish install starts somewhere — an ad impression, a search result, a referral link. But tracking which source actually drove the install is harder than it looks. App store attribution relies on last-click models from ad networks that often over-credit themselves. We wanted something different: a first-party attribution system built on Cloudflare Workers that tells us exactly which marketing channel deserves the credit.

This post walks through how we built it, what we learned, and why first-party attribution beats network SDKs for indie game studios.

The Problem with Ad Network Attribution

Most mobile games rely on networks like Meta Ads, Google Ads, and ironSource to report attribution. The data comes from their SDKs, which means:

- Each network reports its own numbers, and they never match

- Attribution windows vary (1-day vs 7-day vs 28-day click-through)

- Cross-network comparisons are apples to oranges

- You cannot export raw event-level data without paying extra

For CCFish, we were spending more time reconciling attribution reports than actually optimizing campaigns. We needed a single source of truth.

Architecture: Cloudflare Workers + D1

Our attribution system has three layers:

```

[Ad Click] → tracking.link/click?source={network}&campaign={id}

Cloudflare Worker (capture + redirect)

D1: clicks table

[App Launch] → app.ccfish/game/start

SDK sends device_id + install_timestamp

Cloudflare Worker (match install to click)

D1: attributions table

[Revenue Event] → server-side receipt validation

Cloudflare Worker (credit source with revenue)

D1: revenue_events table

```

Layer 1: Click Capture

Every ad campaign points to a custom tracking URL on our Cloudflare Worker:

```typescript

export const onRequest: PagesFunction = async (context) => {

const { request, env } = context;

const url = new URL(request.url);

const click = {

id: crypto.randomUUID(),

source: url.searchParams.get("source"),

campaign: url.searchParams.get("campaign"),

ad_id: url.searchParams.get("ad_id"),

device_id: url.searchParams.get("device_id"),

ip: request.headers.get("CF-Connecting-IP"),

user_agent: request.headers.get("User-Agent"),

timestamp: new Date().toISOString(),

};

await env.DB.prepare(

"INSERT INTO clicks (id, source, campaign, ad_id, device_id, ip, user_agent, timestamp) " +

"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"

).bind(...Object.values(click)).run();

// Redirect to App Store

return Response.redirect("https://apps.apple.com/app/ccfish/id123456789", 302);

};

```

The beauty of Workers is sub-millisecond cold start. The click capture adds negligible latency to the redirect.

Layer 2: Install Matching

When a player launches CCFish, the game sends its device fingerprint via our worker:

```typescript

export const onRequest: PagesFunction = async (context) => {

const { request, env } = context;

const body = await request.json<{ device_id: string; ip: string }>();

// Look for a recent click from this device or IP

const result = await env.DB.prepare(`

SELECT id, source, campaign, timestamp

FROM clicks

WHERE (device_id = ?1 OR ip = ?2)

AND timestamp > datetime('now', '-7 days')

ORDER BY timestamp DESC

LIMIT 1

`).bind(body.device_id, body.ip).first();

if (result) {

await env.DB.prepare(`

INSERT INTO attributions (click_id, device_id, source, campaign, installed_at)

VALUES (?1, ?2, ?3, ?4, ?5)

`).bind(result.id, body.device_id, result.source, result.campaign, new Date().toISOString()).run();

}

return new Response(JSON.stringify({ attributed: !!result }));

};

```

We use a 7-day click-to-install window, which covers 95%+ of organic and paid installs.

Layer 3: Revenue Credits

When a player makes a purchase (with server-side receipt validation), we credit the source responsible for the install:

```sql

SELECT a.source, SUM(r.amount) as total_revenue

FROM revenue_events r

JOIN attributions a ON r.device_id = a.device_id

WHERE r.created_at > datetime('now', '-30 days')

GROUP BY a.source

ORDER BY total_revenue DESC;

```

Revenue-by-source is the single metric that drives our budget allocation. If Meta Ads drives $3 ROAS and TikTok drives $1.20, the math is simple.

D1 Schema Design

| Table | Purpose | Key Columns |

|-------|---------|-------------|

| `clicks` | Raw click events | id, source, campaign, device_id, ip, timestamp |

| `attributions` | Matched installs | click_id, device_id, source, campaign, installed_at |

| `revenue_events` | In-app purchases | id, device_id, amount, product_id, created_at |

All tables are indexed on device_id and timestamp for fast lookups.

Costs

Running this for CCFish costs less than $5/month:

| Service | Monthly Cost | Notes |

|---------|-------------|-------|

| Cloudflare Workers (free tier) | $0 | 100k requests/day included |

| D1 (free tier) | $0 | 5GB storage, 5M read rows/month |

| Tracking domain | $0 | Subdomain of ccfish.app |

| **Total** | **<$5** | Scales to 50k DAU before paid tier |

What This Unlocked

Once attribution data lived in our own D1 database, everything changed:

1. **Real-time campaign dashboards** — Workers render attribution data into server-rendered HTML dashboards. No more waiting for network reports.

2. **Creative-level optimization** — By passing ad_id in tracking URLs, we pinpoint which ad creative drives the best LTV, not just installs.

3. **Cross-network honesty** — When TikTok claims 500 installs and Meta claims 400, but D1 says both came from the same organic search, we know the truth.

4. **ROAS by cohort** — Attributing revenue over a 30-day window gives us true ROAS per channel, not just Day-1 metrics.

The Hybrid Dev+Marketing Takeaway

Building this system required no dedicated data team, no expensive SaaS attribution tool, and no third-party SDKs. Just Cloudflare Workers, D1, and a few hours of TypeScript. For indie game studios like ours, first-party attribution is the difference between gambling on ad spend and investing with confidence.

If you are building a mobile game and relying on network-reported attribution, you are flying blind. Build your own pipeline. It is cheaper than you think and more valuable than any tool you can buy.