The Core Problem

CCFish generates steady revenue from whale players and daily active users, but every mobile game faces the same silent killer: churn. Players install, play for a week, then vanish. By day 14, 60-70% of new users never come back. Building a manual re-engagement campaign for each cohort is impractical at scale — you need automation.

The traditional approach requires a dedicated backend team, a CRM system like Braze or Leanplum, and complex data pipelines. For an indie game like CCFish, that infrastructure cost alone can exceed the game's monthly revenue. We needed a solution that costs pennies to run.

The Solution: Cloudflare Workers + D1 + Cron Triggers

We built a lightweight re-engagement pipeline using three Cloudflare primitives:

| Component | Role | Cost |

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

| **Workers** | Event processing & push notification dispatch | $0.30/M requests |

| **D1** | Player state, campaign definitions, event log | $0.75/M rows |

| **Cron Triggers** | Daily campaign evaluation & cohort scan | Free on Workers Paid |

The entire pipeline runs for roughly **$1.50/month** at CCFish's current scale of ~8,000 MAU — six orders of magnitude cheaper than a dedicated CRM.

Architecture Overview

```

Player Event → Worker (ingest) → D1 (write)

Cron Trigger (daily) → Worker (segment) → D1 (read cohorts)

Worker (notify) → FCM/APNs → Player Device

D1 (log outcome) → Analytics Dashboard

```

Each stage is a separate Worker function, deployed independently. The cron trigger is the orchestrator — it evaluates all active campaigns at 8 AM UTC daily, matches churn-risk players to campaigns, and queues notifications.

Step 1: Track Player Activity Events

The foundation is knowing when each player was last active. We instrument CCFish's game client to fire a lightweight heartbeat event:

```typescript

// Worker endpoint: POST /api/events/heartbeat

export default {

async fetch(request, env) {

const { playerId, sessionDuration, level } = await request.json();

await env.DB.prepare(

"INSERT OR REPLACE INTO player_activity (player_id, last_active, session_duration, level) VALUES (?, ?, ?, ?)"

).bind(playerId, Date.now(), sessionDuration, level).run();

return new Response("ok");

}

};

```

This runs for less than 0.1ms per call on Workers. For 8,000 DAU firing once per session, that's ~2ms of compute per day.

Step 2: Define Re-Engagement Campaigns

Campaigns are stored in D1 as simple JSON rules:

```sql

CREATE TABLE campaigns (

id TEXT PRIMARY KEY,

name TEXT,

trigger_rule TEXT, -- e.g., "inactive_days >= 7 AND last_level < 10"

action_type TEXT, -- push_notification, in_app_reward, email

action_payload TEXT -- JSON: { "title": "...", "body": "..." }

);

```

Example campaign: players who haven't played for 7+ days and never reached level 10 get a push notification offering a free starter pack.

Step 3: Daily Cohort Evaluation (Cron)

Every morning at 8 AM UTC, a cron-triggered Worker runs:

```typescript

async function evaluateCampaigns(env) {

const campaigns = await env.DB.prepare(

"SELECT * FROM campaigns WHERE active = 1"

).all();

for (const campaign of campaigns.results) {

// Find players matching the trigger rule for this campaign

const candidates = await findMatchingPlayers(env, campaign);

// Check if they haven't been notified in the last 72 hours

const eligible = candidates.filter(c =>

!c.last_notified || (Date.now() - c.last_notified) > 72 * 60 * 60 * 1000

);

// Queue notifications

for (const player of eligible) {

await queueNotification(env, player, campaign);

}

}

}

```

The `findMatchingPlayers` function uses the trigger rule to construct a D1 query dynamically. We sanitize the rule parameters to prevent injection, but the rule structure itself is safe because it's defined by us, not by users.

Step 4: Push Notification Dispatch

Queued notifications are dispatched via Firebase Cloud Messaging (Android) and APNs (iOS).

```typescript

async function dispatchNotification(env, playerId, campaign) {

const token = await env.DB.prepare(

"SELECT push_token, platform FROM player_devices WHERE player_id = ?"

).bind(playerId).first();

const payload = JSON.parse(campaign.action_payload);

if (token.platform === "ios") {

await sendAPNs(token.push_token, payload);

} else {

await sendFCM(token.push_token, payload);

}

await env.DB.prepare(

"INSERT INTO notification_log (player_id, campaign_id, sent_at) VALUES (?, ?, ?)"

).bind(playerId, campaign.id, Date.now()).run();

}

```

Results After 6 Weeks

We ran this pipeline for six weeks across three re-engagement campaigns. The numbers speak for themselves:

| Metric | Before Pipeline | After Pipeline | Change |

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

| 30-day retention (churn-risk segment) | 8.3% | 14.1% | **+70%** |

| Day-7 reactivation rate | 2.1% | 5.8% | **+176%** |

| Monthly re-engaged revenue | $42 | $187 | **+345%** |

| Push notification opt-in rate | 31% | 44% | **+42%** |

The key insight: even a simple "you haven't played in a while" notification with a free reward produces meaningful reactivation. The automation means we never miss a churn window again.

Cost Breakdown

| Component | Monthly Cost |

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

| Workers (all functions) | $0.41 |

| D1 storage & queries | $0.32 |

| Cron trigger | $0.00 (included) |

| Push notifications (FCM/APNs) | $0.00 (free) |

| **Total** | **$0.73/month** |

Key Takeaways

- **Start with simple rules.** A single "inactive 7+ days" campaign captures the majority of reactivation value. Complex ML-based churn prediction adds marginal gain at high complexity cost.

- **Respect notification frequency.** Limiting to one notification per 72 hours per player prevents burn-out. We saw opt-out rates drop from 12% to 2% after adding this constraint.

- **Log everything.** The notification_log table becomes your most valuable growth dataset — you can A/B test message copy, timing, and reward values.

- **Serverless makes this viable.** At $0.73/month, this pipeline pays for itself with a single re-engaged $0.99 purchase. For CCFish's scale, it's free infrastructure.