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.