Feature spec
IntermediateCategory
payments-commerce
Build with AI
4–8 hours with Lovable or FlutterFlow
Custom build
1–3 weeks custom dev
Running cost
$0–20/mo (100 users) · $25–80/mo (1,000 users)
Works on
Everything it takes to ship In-App Purchases — parts, prompts, and real costs.
In-app purchases need five components: a paywall screen, a feature gate that checks entitlements, a purchase handler (Stripe on web, RevenueCat + StoreKit/Google Play on mobile), an entitlements table in Supabase, and an idempotent webhook listener. With Lovable or V0 you ship a working web purchase flow in 4–8 hours. On mobile, RevenueCat handles Apple and Google receipt validation. Store commissions (15–30%) are the dominant cost, not ops — which run $0–80/mo depending on scale.
What In-App Purchases Actually Are
In-app purchases (IAP) are the mechanism that converts free users into paying customers by gating premium features, content, or consumables behind a payment. On web, IAP is a Stripe Payment Intent or Checkout session — simple and fully under your control. On mobile (iOS and Android), it is a platform-controlled process: Apple and Google collect the payment, validate the receipt, and take a 15–30% commission before passing you the result. RevenueCat sits between your app and both stores, handling receipt validation, cross-platform entitlement sync, and webhook delivery so you do not have to implement Apple's and Google's separate APIs from scratch. The real product decisions are whether you need one-time purchases or subscriptions, how to gate features server-side rather than client-side, and how to handle the edge cases — restore purchases, expired subscriptions, and duplicate webhook delivery — before they become support tickets.
What users consider table stakes in 2026
- Paywall screen that shows a preview of the premium feature before asking for payment — users need to understand what they are buying
- One-tap purchase with a saved payment method (Apple Pay, Google Pay, or saved card) — no re-entering card details for returning users
- Immediate feature unlock the moment purchase is confirmed — no 'check back in a few minutes' messaging
- Restore purchases button visible in account settings for users who reinstalled the app or switched devices
- Price displayed in the user's local currency with no conversion math required — Apple and Google handle localization; Stripe does via Payment Element
- Purchase history accessible in account settings showing product name, date, and status (active/expired/refunded)
Anatomy of the Feature
Six components. The entitlements table and webhook handler are where most AI-generated IAP builds fail under real usage — read them carefully before prompting.
Paywall and upsell screen
UIA modal (shadcn/ui Dialog on web) or dedicated page that shows a preview of the premium feature, pricing tiers with their feature lists, and a primary CTA. Triggered by a feature gate check whenever the user attempts to access a locked feature. Shows the user's current plan for returning visitors.
Note: Check entitlements before rendering the paywall — show it only to users who do not already have access. Showing a paywall to an active subscriber is a top source of trust-destroying bug reports.
Feature gate middleware
UIClient-side check against the user's entitlements — either loaded from the Supabase user_entitlements table on app init or from the RevenueCat SDK's CustomerInfo object. Blocks navigation to premium routes or renders a locked state over premium UI elements until purchase is confirmed.
Note: Never rely on the client-side gate alone. Add a server-side entitlement check in every API route or Server Action that returns premium content — the client-side gate is UX, not security.
Purchase handler
BackendWeb: a Supabase Edge Function or Next.js API route calls stripe.paymentIntents.create() or stripe.checkout.sessions.create() server-side with the product ID and user metadata. Mobile: RevenueCat SDK calls Purchases.purchasePackage() which wraps Apple StoreKit 2 and Google Play Billing Library 5 — receipt validation is automatic.
Note: RevenueCat handles the iOS/Android split so you write one cross-platform purchase call. For web, use Stripe Checkout for fastest implementation or Payment Intent for embedded checkout.
Entitlements table
DataSupabase user_entitlements table: user_id, product_id, purchased_at, expires_at (null for lifetime purchases), platform (web/ios/android), receipt_id for deduplication, created_at. RLS restricts SELECT to the row owner. RevenueCat webhooks and Stripe webhooks both write to this table via an Edge Function.
Note: The receipt_id column is what makes your webhook idempotent — use it in an INSERT ... ON CONFLICT DO NOTHING to ignore duplicate event delivery.
RevenueCat webhook listener
BackendA Supabase Edge Function receives INITIAL_PURCHASE, RENEWAL, EXPIRATION, CANCELLATION, and REFUND events from RevenueCat. Updates user_entitlements accordingly — inserting on purchase, setting expires_at on expiration, and soft-deleting on refund. For web Stripe purchases, the Stripe payment_intent.succeeded webhook does the same work.
Note: RevenueCat retries webhooks on 5xx. Make your handler idempotent: check receipt_id before inserting. Return 200 immediately and process async if the operation is slow.
Restore purchases flow
UIA 'Restore Purchases' button in account settings calls RevenueCat's Purchases.restorePurchases() on mobile. RevenueCat re-syncs the user's Apple or Google purchase history and updates entitlements. On web, restoration means re-fetching the Stripe payment history for the user's account. Shows a confirmation toast on success.
Note: Apple's App Review requires a visible restore purchases mechanism for any app with non-consumable IAP. Missing this is an automatic rejection reason.
The data model
Two tables cover the full IAP lifecycle. Run this in the Supabase SQL editor:
1create table public.products (2 id uuid primary key default gen_random_uuid(),3 name text not null,4 description text,5 price_cents integer not null check (price_cents >= 50),6 currency text not null default 'usd',7 store_product_id text not null,8 type text not null check (type in ('one_time', 'subscription')),9 created_at timestamptz not null default now()10);1112create table public.user_entitlements (13 id uuid primary key default gen_random_uuid(),14 user_id uuid references auth.users(id) on delete cascade not null,15 product_id uuid references public.products(id) not null,16 purchased_at timestamptz not null default now(),17 expires_at timestamptz,18 platform text not null check (platform in ('web', 'ios', 'android')),19 receipt_id text unique not null,20 created_at timestamptz not null default now()21);2223alter table public.user_entitlements enable row level security;2425create policy "Users can view own entitlements"26 on public.user_entitlements for select27 using (auth.uid() = user_id);2829create index entitlements_user_active_idx30 on public.user_entitlements (user_id, expires_at)31 where expires_at is null or expires_at > now();Heads up: The partial index on user_id + expires_at makes the active entitlement check fast at any user scale. The UNIQUE constraint on receipt_id is the idempotency key — duplicate webhook delivery silently does nothing.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Full control over multi-platform IAP — web via Stripe, iOS via StoreKit 2, Android via Google Play Billing Library 5, unified via RevenueCat with custom receipt validation and analytics.
Step by step
- 1RevenueCat SDK integrated on all three platforms (web, iOS, Android) with a shared entitlement identifier so one purchase unlocks across platforms
- 2Custom receipt validation without RevenueCat for compliance requirements that prohibit third-party processing of purchase receipts
- 3Conversion funnel analytics: track paywall views, purchase initiations, completions, and abandonment rates in a custom analytics table
- 4Family sharing and gifting IAP flows via StoreKit 2's Transaction.updates listener
Where this path bites
- Apple and Google App Review adds 1–7 days to launch timeline; factor this into your schedule
- 2–3 weeks minimum for a production cross-platform IAP implementation including store configuration, sandbox testing, and webhook reliability testing
Third-party services you'll need
Four services cover the full IAP stack. Platform commissions (Apple and Google) are the dominant cost — not operational services.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Stripe | Web payment processing for web-based in-app purchases and subscriptions | Free account; test mode unlimited | 2.9% + $0.30/charge; Billing 0.5–0.8% for subscriptions |
| RevenueCat | Mobile IAP abstraction layer — wraps Apple StoreKit 2 and Google Play Billing Library 5; handles receipt validation, cross-platform entitlements, and webhooks | Free up to $2,500 MRR | 1% of revenue above $2,500 MRR (approx) |
| Apple App Store | iOS payment processing — required for all digital content sold in iOS apps | N/A | 15% for small business (< $1M/yr revenue), 30% otherwise |
| Google Play | Android payment processing — required for all digital content sold in Android apps | N/A | 15% for first $1M/yr revenue per developer, 30% above |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
RevenueCat free tier; Stripe fees only on purchases (~$3–10 depending on volume); Supabase free tier. Near-zero fixed cost at this scale.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Feature unlocked before webhook confirms payment — users who close tab mid-flow lose access
Symptom: AI tools commonly unlock the premium feature immediately when the browser returns from the Stripe Checkout success URL — reading a ?payment_intent_status=succeeded query parameter or checking localStorage. If the user closes the tab before the success URL loads, or if the redirect takes more than a few seconds, the entitlement is never written to the database. The user paid but has no access.
Fix: Never grant entitlements from the client-side redirect URL or any client-readable parameter. Only update entitlements inside the payment_intent.succeeded webhook handler. On the success URL, show a 'Payment confirmed — unlocking your feature...' screen that polls a lightweight GET /api/entitlements endpoint (with 2-second intervals and a 30-second timeout) until the entitlement appears. If it does not appear within 30 seconds, show a 'Contact support' message with the payment intent ID.
Duplicate webhook events unlock entitlement twice — or crash the handler
Symptom: Stripe retries webhook delivery if your handler returns a 5xx status or times out. RevenueCat does the same. If your INSERT into user_entitlements does not handle the case where the receipt_id already exists, the second delivery either inserts a duplicate row (two entitlements for one purchase) or throws a unique constraint violation that crashes the handler and triggers infinite retries.
Fix: Always use INSERT INTO user_entitlements (...) VALUES (...) ON CONFLICT (receipt_id) DO NOTHING. This is a single SQL statement — there is no race condition between the check and the insert. Return 200 from the webhook handler even when the conflict fires; returning 5xx on a duplicate causes Stripe to retry forever.
Apple sandbox receipt rejected in production — 21007 error
Symptom: Apple's receipt validation has two endpoints: the sandbox endpoint and the production endpoint. Receipts generated in the TestFlight or Xcode sandbox environment are only valid against the sandbox endpoint. AI tools that implement manual receipt validation without RevenueCat use only the production endpoint, causing all sandbox receipts to return a 21007 error and block your QA process entirely.
Fix: Use RevenueCat — it handles this automatically by detecting the receipt environment. If you must implement manual validation, follow Apple's documented pattern: first POST to the production endpoint; if you receive a 21007 status, re-POST to the sandbox endpoint. Never hardcode either endpoint; let Apple's response tell you which environment the receipt belongs to.
Feature gate only in the UI — API routes still serve premium content
Symptom: AI tools typically add the feature gate in React — hiding a button or disabling a route in the client-side router. A user who discovers the API endpoint directly (or inspects network requests) can fetch premium content without a valid entitlement. This is not a theoretical attack; it is how determined users bypass paywalls.
Fix: Add an entitlement check in every Server Action and API route that returns premium content. In Next.js: const entitlement = await supabase.from('user_entitlements').select().eq('user_id', session.user.id).eq('product_id', productId).single(); if (!entitlement.data) return new Response('Unauthorized', { status: 403 }). The UI gate is UX; the server check is security.
Paywall renders for already-subscribed users on page load
Symptom: AI tools often load the paywall component unconditionally and check entitlements asynchronously — resulting in a flash of the paywall UI before the check resolves for paying customers. This is jarring and erodes trust, especially for premium-tier users who expect seamless access.
Fix: Load entitlements before rendering any gated UI: in Next.js, fetch entitlements in a Server Component and pass them as props; in FlutterFlow, populate App State on launch via a Custom Action that calls RevenueCat's Purchases.getCustomerInfo() before navigating to the main screen. Use a skeleton loader during the check; never render the paywall while the entitlement check is pending.
Best practices
Check entitlements server-side in every API route or Server Action that returns premium content — the client-side gate is UX, not access control
Make your webhook handler idempotent using INSERT ... ON CONFLICT (receipt_id) DO NOTHING — this handles duplicate delivery without crashing or double-granting
Never unlock features from a URL redirect parameter or client-side flag — always wait for the webhook to confirm payment before writing the entitlement
Show the paywall only to users who genuinely lack entitlements — check on app launch and cache in context; showing a paywall to a paying customer destroys trust
Test the restore-purchases flow before App Store submission — Apple rejects apps that lack a visible restore mechanism for non-consumable IAP
Store the store_product_id in your products table and validate it server-side before creating a Payment Intent — never trust a price passed from the client
Display a human-readable feature list on the paywall, not just a price — users who understand what they are buying convert at 2–3x the rate of price-only paywalls
Implement graceful downgrade on subscription expiry: revoke access to premium features but preserve the user's data; never delete content when a subscription lapses
When You Need Custom Development
AI tools cover standard one-time and subscription IAP well. Specific scenarios require custom development:
- Multi-currency pricing with local payment methods — SEPA Direct Debit in Europe, UPI in India, iDEAL in the Netherlands require payment method-specific Stripe integrations beyond standard card checkout
- Apple App Store Small Business Program qualification requiring precise revenue tracking and automated application renewal — the program reduces Apple's commission from 30% to 15% but requires careful annual revenue verification
- Server-side receipt validation without RevenueCat — some compliance requirements prohibit routing purchase data through a third-party service; custom Apple/Google API integration takes 1–2 weeks
- Family sharing or gifting IAP flows via StoreKit 2's Transaction.updates listener and shared purchase entitlements across linked Apple accounts
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
Do I need to use Apple or Google payment for in-app purchases on mobile?
For digital content, features, and subscriptions sold within iOS and Android apps: yes, Apple and Google mandate their own payment systems. You cannot use Stripe directly for IAP in a mobile app without violating App Store and Play Store policies. The exception is physical goods and services consumed outside the app (like Uber rides or Amazon orders) where Apple and Google explicitly allow alternative payment methods.
What's the difference between a consumable and non-consumable IAP?
A consumable IAP (like 100 coins or 5 extra lives) is used up and can be re-purchased. A non-consumable (like removing ads or unlocking a premium tier) is purchased once and kept forever — it must be restorable via the restore-purchases flow. Subscriptions are a third type: they recur until cancelled. Each type has different handling in StoreKit, Google Play Billing, and RevenueCat — specify which type you need in your prompt.
How do I test purchases without real money?
On iOS: create a Sandbox Tester account in App Store Connect, sign out of your real Apple ID on the test device, sign in with the sandbox account, and make purchases — no real money moves. On Android: add test accounts in Google Play Console and your test device's email must match. Stripe test mode uses card 4242 4242 4242 4242 for web purchases. RevenueCat provides a sandbox environment that mirrors these behaviors.
What percentage does Apple or Google take?
Apple takes 30% by default, reduced to 15% under the App Store Small Business Program for developers who earn under $1M per year in the previous calendar year. You must apply annually. Google Play takes 15% on your first $1M in revenue per year, and 30% above that threshold. Both percentages apply to the net transaction amount after taxes in some regions.
How do I handle refunds for in-app purchases?
Apple processes refund requests directly through Apple Support — you have no control over the refund decision. When Apple issues a refund, RevenueCat receives a REFUND event and sends it to your webhook; update the entitlement expires_at to the current time to revoke access. Google Play allows you to issue refunds from the Play Console within 48 hours. For web Stripe purchases, you initiate refunds via stripe.refunds.create() — the user loses access when your webhook handles the charge.refunded event.
Can I sell the same content on web and mobile with one purchase?
Only if you use RevenueCat to sync entitlements across platforms. When a user purchases on iOS, RevenueCat webhooks to your server and writes the entitlement. When they log in on web, your server reads the same entitlement. However, Apple and Google do not allow cross-platform purchases directly — a purchase made on web via Stripe cannot be recognized by Apple or Google as a completed IAP. You typically offer separate but linked purchases for each platform.
What is RevenueCat and do I need it?
RevenueCat is a subscription and IAP management layer that wraps Apple StoreKit and Google Play Billing. It handles receipt validation (so you do not have to call Apple or Google's APIs directly), provides a unified CustomerInfo object with all entitlements, sends webhooks to your server on purchase events, and shows analytics dashboards. You do not strictly need it — you can call StoreKit and Google Play APIs directly — but doing so correctly takes 3–5 additional weeks of development. For most apps, RevenueCat's free tier covers early-stage volume and is well worth the integration.
How do I prevent users from bypassing the paywall?
Add a server-side entitlement check in every API route or Server Action that returns premium content. The UI-only gate (hiding a button or disabling a route) is trivially bypassed by anyone who can read network requests. In Next.js, use a Server Action that queries Supabase for a valid entitlement row before returning premium data. In FlutterFlow, call your Supabase Edge Function from the API call action and check the response before rendering premium content.
Need this feature production-ready?
RapidDev builds in-app purchases into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.