CCFish grew App Store subscription revenue 3x by replacing a single-SKU auto-renewable subscription with a tiered IAP architecture featuring introductory pricing, server-side receipt validation via Cloudflare Workers, and analytics-driven cohort iteration — all without adding a dedicated backend engineer.

The Problem

CCFish's original iOS subscription was a single product at $3.99/month with no free trial, no introductory offer, and no server-side receipt validation. This caused three problems:

**No conversion funnel.** Without tiered products, the team couldn't measure price elasticity. Receipt parsing happened ad-hoc on the client; the server only checked "does this user have a valid receipt?" — a boolean. No cohort analysis, no pricing data.

**No upgrade path.** Power users who wanted higher-quality fish images, faster identification, or API access had no way to pay more. A single tier capped ARPU at $3.99, leaving money on the table.

**No introductory pricing.** App Store guidelines allow free trials and introductory offers, but the original codebase never implemented them. First-30-day churn was 68% — typical for apps without a hook period that lets users experience value before paying.

````

Before: single-tier revenue structure

SUBSCRIPTION_PRODUCTS:

- product_id: "com.ccfish.sub.monthly"

price: 3.99

introductory_offer: null

free_trial_days: 0

After: tiered structure with offers

SUBSCRIPTION_PRODUCTS:

- product_id: "com.ccfish.sub.basic.monthly"

price: 2.99

introductory_offer: pay_as_you_go, 3mo @ $1.99

free_trial_days: 7

- product_id: "com.ccfish.sub.pro.monthly"

price: 7.99

introductory_offer: pay_as_you_go, 3mo @ $3.99

free_trial_days: 7

- product_id: "com.ccfish.sub.pro.yearly"

price: 59.99

free_trial_days: 14

````

The Solution

The solution had three pillars: product restructuring, server-side receipt validation, and analytics-driven iteration.

**Product restructuring.** Three subscription products replaced the single SKU: Basic Monthly ($2.99), Pro Monthly ($7.99), and Pro Yearly ($59.99). Each had App Store-localized pricing across 28 regions. New subscribers received a 7-day free trial on monthly tiers, and Basic subscribers got a 3-month introductory price of $1.99/month.

**Server-side receipt validation.** Receipt parsing moved from client code to a Cloudflare Worker. The Worker validates receipts against Apple's production and sandbox endpoints, extracts the `original_transaction_id`, and stores results in D1. This eliminated receipt forgery (a known attack on jailed iOS devices) and gave CCFish a canonical subscription record for the first time.

**Analytics pipeline.** Every subscription event — purchase, renewal, cancellation, billing retry — is ingested into D1 and summarized into daily cohort snapshots. This gave the team the first real view into conversion rates by product tier, storefront region, and acquisition source.

````swift

// Core receipt validation worker (Cloudflare Worker)

export default {

async fetch(request: Request, env: Env): Promise<Response> {

const { receiptData, userId } = await request.json();

// 1. Validate against Apple

const appleResponse = await fetch(

env.APPSTORE_SANDBOX

? 'https://sandbox.itunes.apple.com/verifyReceipt'

: 'https://buy.itunes.apple.com/verifyReceipt',

{

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: JSON.stringify({

'receipt-data': receiptData,

password: env.APPSTORE_SHARED_SECRET,

}),

}

);

const apple = await appleResponse.json();

if (apple.status !== 0) return handleError(apple);

// 2. Extract subscription info

const latest = apple.latest_receipt_info?.slice(-1)[0];

const sub = {

productId: latest.product_id,

originalTransactionId: latest.original_transaction_id,

expiresDate: new Date(parseInt(latest.expires_date_ms)),

isTrialPeriod: latest.is_trial_period === 'true',

};

// 3. Store in D1 for analytics

await env.DB.prepare(`

INSERT INTO subscription_events

(user_id, original_transaction_id, product_id, expires_at, is_trial, validated_at)

VALUES (?, ?, ?, ?, ?, datetime('now'))

`).bind(userId, sub.originalTransactionId, sub.productId,

sub.expiresDate.toISOString(), sub.isTrialPeriod ? 1 : 0).run();

return new Response(JSON.stringify({

valid: sub.expiresDate > new Date(),

tier: sub.productId.includes('pro') ? 'pro' : 'basic',

}));

},

};

````

Architecture / Implementation

The system uses three Cloudflare layers: Workers (API), D1 (subscription state + analytics), and KV (entitlement cache).

**Data model.** Two core D1 tables:

````sql

-- Append-only ledger of every validation

CREATE TABLE subscription_events (

id INTEGER PRIMARY KEY AUTOINCREMENT,

user_id TEXT NOT NULL,

original_transaction_id TEXT NOT NULL,

product_id TEXT NOT NULL,

expires_at TEXT NOT NULL,

is_trial INTEGER DEFAULT 0,

validated_at TEXT NOT NULL DEFAULT (datetime('now'))

);

-- Materialized daily cohort aggregates

CREATE TABLE subscription_analytics_daily (

date TEXT NOT NULL,

product_id TEXT NOT NULL,

new_subscribers INTEGER DEFAULT 0,

renewals INTEGER DEFAULT 0,

cancellations INTEGER DEFAULT 0,

active_subscribers INTEGER DEFAULT 0,

mrr_cents INTEGER DEFAULT 0,

PRIMARY KEY (date, product_id)

);

````

**Client flow.** iOS uses StoreKit 2's `Product` API for fetching and `Transaction.updates` for listening. On each event, the app calls the Worker and stores the verdict locally. If the Worker is unreachable, cached entitlement data (24-hour TTL) serves as fallback.

**Deployment.** The Worker and D1 database deploy via Wrangler CLI:

````bash

wrangler.toml subscription service config

name = "ccfish-subscriptions"

main = "src/validate-receipt.ts"

compatibility_date = "2026-04-01"

[[d1_databases]]

binding = "DB"

database_name = "ccfish-subscriptions"

database_id = "<uuid>"

[[kv_namespaces]]

binding = "ENTITLEMENTS"

id = "<kv-namespace-id>"

Deploy

wrangler deploy --env production

wrangler d1 migrations apply ccfish-subscriptions --env production

````

Entitlement tokens are cached in KV with a 15-minute TTL, reducing D1 read volume by 92%. On expiry, the Worker invalidates the KV key on the next validation request.

Results / Metrics

The changes shipped incrementally over six weeks. Before-and-after comparison:

| Metric | Before | After | Change |

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

| Monthly Active Subscribers | 1,240 | 3,842 | **+210%** |

| MRR | $4,948 | $16,237 | **+228%** |

| ARPU | $3.99 | $4.23 | **+6%** |

| 30-Day Trial Conversion | — | 34% | **New** |

| First-30-Day Churn | 68% | 41% | **−27pp** |

| Pro Tier Share | 0% | 23% | **New** |

| Yearly Plan Take Rate | — | 18% | **New** |

| Receipt Fraud Rate | ~5% | <0.1% | **−97%** |

The 3x revenue growth came from two compounding effects. First, introductory pricing reduced the activation barrier — the 7-day free trial + $1.99 introductory price drove a 3.1x increase in new subscriber starts. Second, the Pro tier captured power users who previously had no way to pay more. 23% of new subscribers chose Pro at $7.99/month, and 18% chose the yearly plan.

````

Migration timeline (6 weeks)

Week 1-2: Product setup in App Store Connect + introductory pricing

Week 3: Worker receipt validation + D1 schema to staging

Week 4: iOS StoreKit 2 integration

Week 5: Soft launch (10% of users) — caught two edge cases

Week 6: Full rollout + deprecated old client-side parsing

````

The additional Cloudflare infrastructure added $47/month in operating costs. The previous client-side parsing cost $0 in infra but ~20 engineer-hours/month debugging receipt failures. Net operational cost was negative.

Key Takeaways

1. **Tiered pricing captures willingness-to-pay variance.** 23% of users were willing to pay 2x+ more. Without tiers, that revenue was invisible.

2. **Introductory pricing is a conversion lever, not a revenue sacrifice.** The 3-month reduced price increased gross subscriber starts enough that total cohort revenue exceeded the single-tier baseline within 6 months.

3. **Server-side receipt validation is mandatory for production apps.** Moving validation to a Cloudflare Worker eliminated fraud and gave a canonical subscription record for analytics, support, and refund management.

4. **Start with the data model.** The `subscription_analytics_daily` table made it possible to answer "how do Japanese Basic subscribers compare to US Pro subscribers?" in a single query.

5. **Cloudflare Workers are an excellent fit for subscription middleware.** Receipt validation is I/O-bound (HTTP calls to Apple, D1 writes, KV cache) and benefits from edge distribution. No dedicated backend team needed — the entire system runs on ~400 lines of TypeScript at $47/month.

6. **Incremental rollout is essential for subscription changes.** The 10% soft launch caught two edge cases — sandbox receipts from TestFlight and Mac App Store purchases — before they affected production users.