I built my own IAP backend instead of using RevenueCat β what 3 weeks of pain taught me
I built my own IAP backend instead of using RevenueCat β what 3 weeks of pain taught me
Current Situation Analysis
Shipping a subscription-based React Native app forces a critical architectural decision: adopt a third-party IAP manager like RevenueCat or build a custom backend. While third-party solutions abstract away vendor complexity, they introduce recurring cost scaling (1% post-$2.5K MRR), architectural redundancy (mirroring subscription state to your own Postgres via webhooks anyway), and data ownership constraints. Traditional tutorials and community guides frequently fail to address production-grade edge cases: incomplete JWT/JWS chain validation, reliance on deprecated Google Play API v1 endpoints, naive state inference from timestamps, and the critical omission of purchase acknowledgement flows. Solo developers quickly encounter state drift, unexpected refunds, and divergent vendor lifecycle events that break simple boolean gating logic. The hidden complexity of reconciling Apple's and Google's distinct grace periods, holds, and notification reliability turns a "quick MVP" into a multi-week engineering sprint.
WOW Moment: Key Findings
| Approach | Architecture Latency | Data Ownership | State Classification Accuracy | Initial Setup Time | Long-term Cost (Annual at $10K MRR) |
|---|---|---|---|---|---|
| RevenueCat (Third-party) | ~150-300ms (extra hop) | External DB + Webhook Mirror | High (vendor-abstracted) | < 1 day | ~$1,200 (1% fee) |
Custom onesub Backend |
~20-50ms (direct API) | Full Postgres/Local Control | High (unified enum mapping) | ~3 weeks | $0 (self-hosted) |
Key Findings:
- Direct API integration eliminates the webhook hop, reducing state resolution latency by ~60-80%.
- A unified state machine accurately maps vendor-specific enums (
DID_FAIL_TO_RENEWwith grace subtypes,IN_GRACE_PERIOD,ON_HOLD,SUBSCRIPTION_PAUSED) without losing UX context. - Hybrid validation (webhooks as fast path + direct API fetch as source of truth) reduces state drift to near-zero in production.
- Self-hosting removes recurring revenue share, making the custom approach cost-positive at ~$8K+ MRR after initial engineering investment.
Core Solution
The production-ready backend abstracts vendor-specific IAP flows into a pluggable middleware layer. Key technical implementations include:
1. Apple StoreKit 2 JWS Verification
Full certificate chain validation is mandatory. The implementation walks the x5c chain in the JWT header, verifies each certificate against Apple Root CA G3, and validates the JWT signature against the leaf cert's public key. Payload decoding without chain verification is explicitly rejected.
2. Google Play Developer API v3 Integration
OAuth2 service account authentication is paired with purchases.subscriptionsv2.get. This endpoint returns a subscriptionState enum that maps cleanly to lifecycle states. The solution explicitly avoids v1 patterns and never infers state from expiryTimeMillis + cancelReason.
3. Unified Lifecycle State Machine
Vendor events are normalized into a single state machine that separates active: boolean (for feature gating) from raw state strings (for UX messaging). This allows accurate differentiation between "card failed but access remains" (grace period) and "subscription on hold" (payment suspension).
4. Webhook + Direct Fetch Fallback Strategy
App Store Server Notifications V2 and Google RTDN are treated as the "fast path" but not the sole source of truth. Missed notifications are recovered via direct API calls on /status checks (subscriptionsv2.get for Google, App Store Server API for Apple).
5. Package Extraction & Integration
The working backend is extracted into an MIT-licensed package with a pluggable subscription store (PostgreSQL built-in, interface available for Redis/other backends) and an optional React Native SDK (useOneSub() hook + paywall component).
app.use(createOneSubMiddleware(config));
Pitfall Guide
- Incomplete JWS Certificate Chain Verification: Decoding the JWT payload without walking the
x5cchain, verifying against Apple Root CA G3, and validating the signature against the leaf cert's public key leaves the backend vulnerable to forged receipts. - Using Google Play API v1 or Inferred States: Relying on
expiryTimeMillis+cancelReasonto determine subscription status is unreliable. Always usepurchases.subscriptionsv2.getand read thesubscriptionStateenum directly. - The 3-Day Acknowledgement Trap: Google automatically refunds any purchase not acknowledged within 3 days. Subscriptions require
acknowledgePurchasecalls just like one-time IAP; missing this causes silent revenue loss. - Treating Webhooks as the Sole Source of Truth: App Store Server Notifications V2 and Google RTDN can drop or arrive out of order. Implement a hybrid model where webhooks trigger fast updates, but direct API fetches serve as the authoritative state resolver.
- Naive Vendor State Mapping: Apple's
DID_FAIL_TO_RENEW(withGRACE_PERIODvsGRACE_PERIOD_EXPIRED) and Google'sIN_GRACE_PERIOD,ON_HOLD,SUBSCRIPTION_PAUSEDcannot be collapsed into a single boolean without losing critical UX context. Separate gating logic from display state. - Underestimating Analytics & Dashboard Requirements: Custom backends lack built-in cohort retention, LTV tracking, and experiment management. Plan for operational dashboards (active counts, failed webhook retries, state drift alerts) early to avoid blind spots.
- Ignoring Family Sharing & Promotional Offers: These features introduce complex entitlement chains, shared subscription states, and dynamic pricing tiers that break standard receipt validation flows if not explicitly handled in the state machine.
Deliverables
- Architecture Blueprint: Detailed system diagram covering webhook ingestion, JWS chain validation pipeline, Google v3 API routing, unified state machine transitions, and direct-fetch fallback mechanisms.
- Production Checklist: Step-by-step verification guide including certificate chain validation,
subscriptionStateenum mapping,acknowledgePurchaseflow implementation, webhook retry configuration, and state drift recovery procedures. - Configuration Templates: Ready-to-use Postgres schema for subscription state storage,
createOneSubMiddlewareconfig examples, pluggable store interface definitions, and React Native SDK initialization snippets for paywall gating and status hooks.
