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.