The Data Stack That Costs $0

When you run an indie game studio, every dollar counts. A $200/month Mixpanel subscription buys you a fraction of what you need. Amplitude Enterprise at $50k/year is out of the question. Even Firebase Analytics, while free, gives you limited control over custom event schemas and no ability to join events with purchase data.

CCFish needed a data pipeline that could:

- Track 50+ unique event types across iOS and Android

- Join player behavior with purchase and ad revenue data

- Power a live marketing dashboard updated every 5 minutes

- Export clean datasets for ad network optimization

- Cost less than a pizza per month

We built it with Cloudflare Workers, D1, and about 200 lines of TypeScript. Here is exactly how it works.

Architecture Overview

```

Player Device → Event Worker → D1 (raw events)

Aggregation Worker (cron, every 5 min)

D1 (aggregated tables: daily_metrics, revenue_summary)

Dashboard Worker (HTTP) → Astro Dashboard

```

The key insight: instead of paying for a data warehouse, we pay for compute only when data is processed. Cloudflare Workers cost $0 on the free tier for our volume (under 50k requests/day).

Database Schema

Three core tables power the pipeline:

1. Raw Events Table

```sql

CREATE TABLE game_events (

id TEXT PRIMARY KEY,

player_id TEXT NOT NULL,

event_type TEXT NOT NULL,

session_id TEXT,

metadata TEXT, -- JSON blob, schema varies by event_type

client_ts INTEGER, -- Unix ms from device

server_ts INTEGER DEFAULT (unixepoch() * 1000),

platform TEXT -- 'ios', 'android', 'web'

);

CREATE INDEX idx_events_player ON game_events(player_id, event_type);

CREATE INDEX idx_events_type_ts ON game_events(event_type, server_ts);

```

2. Daily Aggregated Metrics

```sql

CREATE TABLE daily_metrics (

date TEXT NOT NULL,

metric_name TEXT NOT NULL,

metric_value REAL NOT NULL,

platform TEXT,

PRIMARY KEY (date, metric_name, COALESCE(platform, ''))

);

```

3. Revenue Summary

```sql

CREATE TABLE revenue_summary (

date TEXT NOT NULL,

source TEXT NOT NULL, -- 'iap', 'ad', 'subscription'

amount_cents INTEGER NOT NULL,

player_count INTEGER NOT NULL,

PRIMARY KEY (date, source)

);

```

The Ingestion Worker

Events come in as POST requests to a single Cloudflare Worker endpoint. The worker validates, enriches, and inserts:

```typescript

export async function onRequest(context) {

const { request, env } = context;

if (request.method !== "POST") {

return new Response("Method not allowed", { status: 405 });

}

const events = await request.json();

const batch = Array.isArray(events) ? events : [events];

const stmt = env.DB.prepare(

`INSERT OR IGNORE INTO game_events

(id, player_id, event_type, session_id, metadata, client_ts, platform)

VALUES (?, ?, ?, ?, ?, ?, ?)`

);

const results = await env.DB.batch(

batch.map(e => stmt.bind(

crypto.randomUUID(),

e.player_id,

e.event_type,

e.session_id || null,

JSON.stringify(e.metadata || {}),

e.timestamp || Date.now(),

e.platform || 'unknown'

))

);

return new Response(JSON.stringify({

inserted: results.filter(r => r.success).length,

total: batch.length

}), { headers: { "Content-Type": "application/json" } });

}

```

Batch inserts are critical — D1 batch operations are atomic and significantly faster than individual inserts.

The Aggregation Pipeline

Every 5 minutes, a Cron Trigger Worker aggregates the raw data:

```typescript

export async function handleCron() {

const env = getEnv();

// Daily active players

await env.DB.prepare(`

INSERT OR REPLACE INTO daily_metrics (date, metric_name, metric_value, platform)

SELECT

date('now') as date,

'dau' as metric_name,

COUNT(DISTINCT player_id) as metric_value,

COALESCE(platform, 'all') as platform

FROM game_events

WHERE server_ts > unixepoch() * 1000 - 86400000

GROUP BY platform

`).run();

// Revenue from IAP

await env.DB.prepare(`

INSERT OR REPLACE INTO revenue_summary (date, source, amount_cents, player_count)

SELECT

date('now') as date,

'iap' as source,

SUM(CAST(JSON_EXTRACT(metadata, '$.amount_cents') AS INTEGER)) as amount_cents,

COUNT(DISTINCT player_id) as player_count

FROM game_events

WHERE event_type = 'purchase'

AND server_ts > unixepoch() * 1000 - 86400000

`).run();

}

```

This cron approach keeps query performance fast — the dashboard queries pre-aggregated tables instead of scanning millions of raw events.

The Marketing Dashboard

The dashboard queries live aggregates and caches at the edge for 5 minutes:

```sql

-- Weekly revenue by source

SELECT source, SUM(amount_cents) as total_cents

FROM revenue_summary

WHERE date >= date('now', '-7 days')

GROUP BY source;

-- 7-day retention trend

SELECT

DATE(install_date) as cohort,

ROUND(AVG(CASE WHEN day >= 7 THEN 1.0 ELSE 0 END) * 100, 1) as d7_rate

FROM retention_cohorts

WHERE install_date >= date('now', '-30 days')

GROUP BY cohort

ORDER BY cohort;

```

The Cost Breakdown

| Service | Monthly Cost | What It Handles |

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

| Cloudflare Workers (free) | $0 | Event ingestion, aggregation, dashboard API |

| D1 Database (free) | $0 | 5GB storage, all event + agg data |

| Astro + Cloudflare Pages (free) | $0 | Dashboard frontend, 100k requests/day |

| Domain + DNS | $1/month | ccfish.io analytics subdomain |

| **Total** | **$1/month** | |

Compare to $200/month for Mixpanel Growth or $599/month for Amplitude.

CCFish Numbers After 3 Months

Since deploying the pipeline in February 2026:

- **280k events ingested** across 8,200 players

- **12.4 million rows** in game_events table

- **4.8 GB** total D1 storage used

- **99.97% uptime** — zero data loss events

- **$3** total infrastructure cost to date

The pipeline has powered 5 A/B test analyses, 3 ad network optimizations, and 2 product feature decisions. Each of those decisions directly impacted player retention or revenue.

The Data Flywheel

The real magic happens when the data pipeline closes the loop with marketing:

1. **Pipeline detects** a 20% drop in D7 retention among iOS users

2. **Team investigates** and finds the iOS 18.3 update broke a tutorial step

3. **Dev team fixes** the bug and ships a hotfix

4. **Pipeline confirms** retention recovers to baseline within 3 days

5. **Marketing adjusts** ad targeting to avoid iOS 18.3 users during the window

Without the pipeline, you would not detect the drop until your next App Store review — 2-4 weeks too late.

This is why building your own data infrastructure is not just a cost saving. It is a competitive advantage. When you control every step of the pipeline, you can answer any question about your players within minutes. An indie team with a $1/month pipeline can move faster on data than most studios with a $50k/month data team.