RevenueCat and Stripe in the Same App: Drawing the Line Apple Cares About
How I combined in-app subscriptions (RevenueCat) and direct card payments (Stripe) in production React Native apps without tripping App Store review — and the decision rule that makes it simple.
Two of my production apps take money from users in two completely different ways — sometimes on the same screen flow. Basra TV sells reader subscriptions through RevenueCat and sells newspaper ad placements through Stripe. Harf Media takes Stripe card payments (and cash on delivery) for billboard advertising campaigns. All of it passed App Store review, because the split follows the one rule Apple actually enforces.
The decision rule
Apple's guideline 3.1.1 reduces to a single question:
Is the user buying digital content or functionality consumed inside the app?
- Yes → it must go through in-app purchase (StoreKit — in my case via RevenueCat). Premium articles, subscriptions to app content, unlocking features.
- No — it's a real-world product or service → it must not use IAP; you're free (and expected) to charge cards directly. Physical goods, services performed outside the app, advertising placements, rides, food.
A newspaper subscription is digital content → IAP. A billboard campaign in downtown Basra is about as real-world as a service gets → Stripe. An advertiser buying placement in the paper is buying an advertising service, not app content → Stripe. Once you phrase every purchase this way, the architecture draws itself.
The expensive mistake is building the wrong rail first. Moving a purchase flow from Stripe to IAP late in a project means new entitlement logic, new server-side receipt handling, and a resubmission cycle. Classify every SKU before writing a line of payment code.
The RevenueCat side: entitlements, not receipts
For subscriptions, RevenueCat earns its fee by making the horrible parts someone else's problem: receipt validation, renewal state, grace periods, cross-platform restore. The integration shape that worked for me:
- The app never decides who is premium. It asks for entitlements:
const info = await Purchases.getCustomerInfo()
const isPremium = info.entitlements.active["premium"] !== undefined
- The backend listens to RevenueCat webhooks and mirrors subscription state into its own database. Content-gating decisions on the server come from that mirror, never from the client's claim.
- Restore purchases gets a visible button. Reviewers check for it, and users who reinstall genuinely need it.
The part that still hurts with or without RevenueCat: sandbox testing. Sandbox renewals run on accelerated clocks, App Store sandbox accounts get into wedged states, and Android and iOS sandboxes fail differently. Budget real days for this. My rule of thumb: the purchase flow is done when a fresh install on both platforms can buy, force-quit, restore, and cancel — and the backend's mirror agrees at every step.
The Stripe side: the app is a thin client
For the real-world payments, the design principle is that the mobile app never holds anything sensitive and never computes a price:
- The app requests a payment from the backend (Laravel, in both projects): "campaign X, dates Y–Z."
- The server calculates the amount, creates the PaymentIntent, and returns the client secret.
- The app confirms the payment with Stripe's SDK sheet.
- Fulfillment happens only when the server receives the
payment_intent.succeededwebhook — never on the client's success callback. A client can lie, lose connectivity after paying, or crash mid-flow; the webhook is the source of truth.
One market-specific lesson from Iraq: card penetration is low, so Harf Media also offers cash payment, where the order is placed in a pending_payment state and an admin confirms receipt in the dashboard. Payment architecture isn't just APIs — it's matching how your users actually pay. The state machine (pending → paid → fulfilled, with failed and refunded exits) was designed once and both payment methods feed it.
When both rails meet
Practical details from running both in one app:
- Keep the checkout UIs visually distinct. The IAP sheet is Apple's; the Stripe sheet is yours. Users treat card entry with more suspicion — the Stripe flow shows the exact charge, currency, and what happens next.
- One ledger on the backend. RevenueCat webhooks and Stripe webhooks both write into a single normalized transactions table. When support asks "what has this user paid for?", nobody joins three systems by hand.
- Review notes help. In App Store Connect, one paragraph explaining "subscriptions use IAP; billboard/ad services are physical-world services charged via card, per 3.1.3(e)" preempted the reviewer question both times.
None of this is exotic engineering. It's classification discipline plus server-side truth. Get those two right and mixed-payment apps are, pleasantly, boring.