Skip to main content

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:

TierPriceModel$/monthHoursImagesContext QuiltSummary
free$0Haiku$0.0511offdelta
std$2.99Haiku$1.25251teaserdelta
pro$4.99Haiku$2.50502ondelta
ultra$9.99Sonnet$4.75253onchoice
umax$19.99Sonnet$9.50505onchoice
admininternalSonnetunlimitedinf10onchoice

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):

  1. StoreKit processes the purchase (Apple prorates the charge)
  2. iOS calls POST /v1/verify-receipt with the new product ID
  3. GhostPour sets the new tier and resets allocation to the new tier's full limit
  4. monthly_used_usd resets to 0 — the user gets a fresh allocation
  5. 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.

StoreKit does not push to GhostPour

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

  1. Apple stops renewing the subscription
  2. On next app launch, currentEntitlements returns empty
  3. iOS calls POST /v1/sync-subscription with active_product_id: null
  4. 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.