Subscription System
Three systems collaborate to manage subscriptions: Apple App Store (StoreKit 2), your iOS app, and GhostPour.
Subscription Flow
Tier Definitions
Configured in config/tiers.yml. Five purchasable tiers plus admin:
| Tier | Price | Model | $/month | Hours | Images | Context Quilt | Summary |
|---|---|---|---|---|---|---|---|
| free | $0 | Haiku | $0.05 | 1 | 1 | off | delta |
| std | $2.99 | Haiku | $1.25 | 25 | 1 | teaser | delta |
| pro | $4.99 | Haiku | $2.50 | 50 | 2 | on | delta |
| ultra | $9.99 | Sonnet | $4.75 | 25 | 3 | on | choice |
| umax | $19.99 | Sonnet | $9.50 | 50 | 5 | on | choice |
| admin | internal | Sonnet | unlimited | inf | 10 | on | choice |
How cost limits map to hours:
- Haiku tiers:
$0.05/hour→ $1.25 limit = 25 hours - Sonnet tiers:
$0.19/hour→ $4.75 limit = 25 hours
StoreKit Product IDs
Map these in tiers.yml to connect Apple purchases to your tier system:
# config/tiers.yml example
standard:
storekit_product_id: "com.yourapp.sub.standard.monthly"
monthly_cost_limit_usd: 1.25
default_model: "anthropic/claude-haiku-4-5-20251001"
# ...
Sync on Every App Launch
On every launch, the iOS app checks StoreKit entitlements and reconciles with GhostPour:
Upgrade Behavior
When a user upgrades (e.g., Standard → Pro):
- StoreKit processes the purchase (Apple prorates the charge)
- iOS calls
POST /v1/verify-receiptwith the new product ID - GhostPour sets the new tier and resets allocation to the new tier's full limit
monthly_used_usdresets to 0 — the user gets a fresh allocation- No carryover of unused hours from the old tier
Why no carryover: Apple discounts upgrades (prorated credit). You don't lose money, but you also don't stack hours.
Trial Flow
Why the trial cap exists: Without it, a user could subscribe to a 7-day free trial, burn through all 25 hours in 3 days, then cancel before Apple charges them. The trial cap ($0.50 = 10 hours) limits exposure during the unpaid period.
Apple doesn't notify your server when a trial converts to paid. You only find out when the iOS app launches and calls sync-subscription. The conversion is detected by comparing the server's is_trial flag against StoreKit's current state.
Cancellation
- Apple stops renewing the subscription
- On next app launch,
currentEntitlementsreturns empty - iOS calls
POST /v1/sync-subscriptionwithactive_product_id: null - GhostPour downgrades to free tier, resets allocation
Server-Driven Subscription UI
GET /v1/tiers returns everything your iOS app needs to render the subscription paywall:
{
"tiers": {
"standard": {
"display_name": "Standard",
"description": "Monthly AI meeting assistance for regular use.",
"hours_per_month": 25,
"features": { "context_quilt": "teaser" },
"feature_bullets": [
"~25 hours of AI assistance",
"Claude Haiku - fast and capable",
"Auto-summaries every 10 min"
],
"storekit_product_id": "com.yourapp.sub.standard.monthly"
}
},
"feature_definitions": {
"context_quilt": {
"display_name": "Context Quilt",
"teaser_description": "Your AI found connections to your past meetings",
"upgrade_cta": "Upgrade to Pro to unlock meeting memory"
}
}
}
This keeps your paywall descriptions server-controlled — no App Store release needed to update copy.