In-App Purchases in React Native 2026: RevenueCat vs Rolling Your Own
A practical breakdown of when RevenueCat earns its cut and when a direct StoreKit 2 / Google Play Billing integration is the saner call for React Native teams in 2026.
Every few months a founder asks us the same question: do we really need RevenueCat, or can we just call StoreKit and Play Billing directly? The honest answer is that it depends on how much of your business runs on subscriptions and how much receipt-parsing pain you're willing to own. Here's the breakdown we give clients before we write a single line of code.
The decision isn't really about the SDK
Wiring up a purchase flow in React Native is not the hard part. react-native-iap and Expo's expo-in-app-purchases successor libraries have both matured enough that you can trigger a purchase sheet in an afternoon. The hard part is everything that happens after the user taps Buy:
- Server-side receipt validation that survives Apple's and Google's quirks
- Grace periods, billing retries, and subscription state transitions
- Refunds initiated from the App Store or Play Console (not your app)
- Cross-platform entitlement lookup when the same user pays on iOS but opens on Android web
- Family Sharing on iOS and account holder changes on Google Play
- Introductory offers, promotional offers, and win-back offers
- Proration when a user upgrades or downgrades mid-cycle
If your product has one non-consumable unlock and no subscriptions, none of this matters much. If you sell recurring subscriptions across platforms, this list is your roadmap whether you like it or not.
What RevenueCat actually gives you
RevenueCat is not just a wrapper around StoreKit and Play Billing. It's a receipt-normalization layer plus a hosted entitlements service. You call Purchases.getCustomerInfo() and get back a consistent object regardless of which store the purchase came from. It handles server-to-server notifications from both stores, keeps its own copy of the subscription state, and exposes webhooks so your backend can react to renewals, cancellations, and refunds without parsing App Store Server Notifications v2 payloads yourself.
You pay for that convenience — the current pricing is a percentage of tracked revenue above a free tier. Whether that's a good deal is arithmetic, not ideology.
When rolling your own is the right call
We've shipped both. Direct integration makes sense when at least two of the following are true:
- You're iOS-only or Android-only. Half the value of RevenueCat is cross-store normalization. Kill that requirement and the trade shifts.
- You already have a mature billing backend. If you're a fintech or SaaS extending to mobile, you probably have webhook infrastructure, a subscriptions table, and reconciliation jobs. You don't need another source of truth.
- Your subscription revenue is high enough that the percentage fee exceeds the cost of an engineer maintaining the integration. For most consumer apps this crossover happens later than founders expect. For a well-funded product doing serious ARR, it can happen fast.
- You have compliance requirements that make a third-party receipt custodian awkward. Rare, but real for some regulated verticals.
Here's roughly what a StoreKit 2 server-side validation call looks like from a Node backend, using Apple's JWS-signed transaction format:
import { AppStoreServerAPIClient, Environment } from '@apple/app-store-server-library';
const client = new AppStoreServerAPIClient(
signingKey, // .p8 contents
keyId,
issuerId,
bundleId,
Environment.PRODUCTION
);
export async function verifyTransaction(transactionId: string, userId: string) {
const info = await client.getTransactionInfo(transactionId);
const decoded = decodeJWS(info.signedTransactionInfo);
if (decoded.bundleId !== EXPECTED_BUNDLE_ID) {
throw new Error('Bundle mismatch');
}
await db.entitlements.upsert({
userId,
productId: decoded.productId,
expiresAt: new Date(decoded.expiresDate),
originalTransactionId: decoded.originalTransactionId,
});
}
That's the happy path. The unhappy paths — sandbox vs production environment detection, revoked transactions, subscription upgrades that generate new originalTransactionId values, refund notifications arriving weeks later — are where the engineering hours actually go.
The Google Play side is worse
Play Billing's server-side story improved with the Monetization API, but you still deal with purchase tokens that expire, subscriptionsv2.get returning different shapes for different product types, and Real-Time Developer Notifications that arrive via Pub/Sub with at-least-once delivery. You will build a deduplication table. You will get paged at 3am the first time a notification handler crashes on an unexpected notificationType.
What actually breaks in production
Regardless of which path you pick, these are the failure modes we see most often on React Native projects:
Sandbox vs production confusion on iOS
Apple's TestFlight uses the production environment. Xcode debug builds use sandbox. If your validation logic checks the wrong endpoint first, TestFlight testers will report "purchase succeeded but I'm not premium" and you'll waste a day. StoreKit 2's signed transactions include the environment in the payload, so trust that field rather than the endpoint you called.
Restore purchases on a fresh install
Every App Store reviewer tests this. If a user reinstalls, logs into their account, and their entitlement doesn't come back, you get a rejection. RevenueCat handles this transparently by tying purchases to an anonymous or aliased app user ID. On a DIY setup, you need to either (a) require login before purchase, or (b) implement Transaction.currentEntitlements on iOS and queryPurchasesAsync on Android and re-post them to your backend on launch.
The offline purchase problem
A user buys a subscription, then loses connectivity before your app can tell your backend. StoreKit and Play Billing will happily replay the transaction on next launch, but only if you haven't marked it finished. The rule: never call finishTransaction or acknowledgePurchase until your server has confirmed the entitlement is stored. Get this wrong and you either lose sales or get charged back.
Family Sharing and shared subscriptions
iOS Family Sharing means one purchase can entitle up to six Apple IDs. If your entitlement is keyed by the purchasing Apple ID rather than by originalTransactionId plus the current user, family members won't get access. This is one of those bugs that ships to production quietly because most testers don't use Family Sharing.
A pragmatic middle path
We've had good results with a hybrid approach on medium-sized apps: use RevenueCat as the client SDK and receipt validator, but mirror every entitlement change into our own database via webhooks. That way the app benefits from RevenueCat's cross-platform ergonomics, but our backend has a canonical entitlements table it can join against for feature gating, analytics, and support tooling. If we ever want to migrate off, we already have the data.
The migration path in the other direction — starting DIY and moving to RevenueCat later — is genuinely painful because you have to import historical transactions and reconcile subscription states. Start with the abstraction you think you'll want at 10x your current scale.
Expo-specific notes
If you're on Expo managed workflow, IAP requires a dev client or a bare/prebuild setup — it won't work in Expo Go. RevenueCat ships a config plugin that handles the native module wiring during expo prebuild. Direct integration via react-native-iap also has a plugin but requires more manual entitlement configuration in your app.config.ts. Either way, budget a day for the first EAS Build that includes IAP to actually run end-to-end on a TestFlight build with a real sandbox account.
Where we'd start
If you're shipping a new subscription app in 2026 and revenue is unproven, use RevenueCat. The percentage fee is cheap insurance while you're figuring out pricing, and the analytics dashboards will save you from building the same charts yourself. Mirror the webhook events into your own database from day one so you're not locked in.
If you're above roughly a few hundred thousand in annual subscription revenue and you have a backend team, run the numbers. At that point the fee is real money and a well-scoped DIY integration is a reasonable two-to-three-week project plus ongoing maintenance. Just be honest about the maintenance part — receipt validation is a system, not a feature.
Either way, decide before you start writing purchase code. Retrofitting entitlement logic is where budgets go to die. If you'd like a second pair of eyes on the decision, our mobile team has done this migration in both directions and can usually spot the trap doors within an hour of looking at your product model.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading
React Native New Architecture Migration in 2026: What Actually Breaks
Fabric and TurboModules are the default in 2026, but migrating a real app is still messy. Here's what breaks, what to fix first, and where the interop layer saves you.
Hermes vs JSC in 2026: What Actually Changed for React Native Startup and Memory
Hermes has been the default in React Native for a while, but the JSC-vs-Hermes decision still shows up in real projects. Here's what's changed, what still bites, and when we'd still reach for JSC.
Deep Linking in React Native 2026: Universal Links, App Links, and the Edge Cases That Break Onboarding
Deep links look trivial until a marketing campaign melts down because iOS opens Safari instead of your app. Here's how we wire universal links, App Links, and Expo Router without the usual footguns.
