The Swift Kit logoThe Swift Kit
Comparison

StoreKit 2 vs RevenueCat — Which Should You Use for iOS Subscriptions in 2026?

You are about to add subscriptions to your iOS app. You have two paths: go native with StoreKit 2, or use RevenueCat as a wrapper. This guide breaks down everything — architecture, code, pricing, and the tradeoffs that actually matter — so you can pick the right one for your project.

Ahmed GaganAhmed Gagan
15 min read

The Subscription Landscape in 2026

Subscriptions account for roughly 80% of non-game App Store revenue in 2026. If you are building anything beyond a one-trick utility, you are almost certainly going to need recurring payments. The good news: both Apple and the third-party ecosystem have matured enormously. The bad news: the number of options can be paralyzing.

Two years ago, StoreKit 2 was still missing critical server-side pieces and RevenueCat was the obvious default. That calculus has shifted. Apple shipped App Store Server Library 2.0 and improved StoreKit 2 testing in Xcode 16. RevenueCat, meanwhile, crossed 35,000 apps and launched its own paywall builder. Both paths are now genuinely viable — the right choice depends on your team size, your roadmap, and how much infrastructure you want to own.

I have shipped apps using both approaches. I have also helped dozens of indie developers through The Swift Kit choose between them. This post captures everything I have learned.

What Is StoreKit 2?

StoreKit 2 is Apple's native in-app purchase framework, introduced at WWDC21 alongside iOS 15. It replaced the callback-heavy original StoreKit with a modern Swift-concurrency API built around async/await, Product structs, and Transaction values that are cryptographically signed JWS tokens.

Since launch, Apple has steadily expanded it:

  • iOS 16 added Product.SubscriptionInfo.RenewalState for cleaner subscription status checks and the Message API for price-increase consent.
  • iOS 17 brought Transaction.offer(for:) for programmatic offer codes, plus the SubscriptionStoreView — Apple's own SwiftUI paywall component.
  • iOS 18 introduced the Subscription Status API v2 with richer renewal info, improved StoreKit Testing with simulated server notifications, and the ability to read Transaction.environment to distinguish sandbox from production without a server round-trip.

The bottom line: StoreKit 2 in 2026 is a complete, production-ready framework. You can build a full subscription flow without ever leaving Apple's ecosystem.

What Is RevenueCat?

RevenueCat is a subscription management SDK and backend. You install their Swift package, configure products in their dashboard, and they handle receipt validation, entitlement management, analytics, and webhook delivery on your behalf. Under the hood, RevenueCat uses StoreKit 2 on iOS 15+ devices — so you are still getting Apple's native purchase flow. RevenueCat is the layer on top.

What RevenueCat adds beyond raw StoreKit 2:

  • A unified backend that syncs subscriber status across iOS, Android, web, and Stripe.
  • A real-time analytics dashboard with MRR, churn, cohort retention, trial conversion, and LTV charts.
  • Offerings and experiments — remote configuration of which products to show, with A/B testing built in.
  • Paywalls RC — a server-driven paywall builder that lets you change paywall design without an app update.
  • Webhooks that fire on every subscription lifecycle event, making it trivial to sync state to your own backend.

RevenueCat is free up to $2,500 in tracked monthly revenue (MTR). Above that, pricing starts at $120/month for the Grow plan and scales from there.

The Big Comparison Table

Here is a side-by-side breakdown across every dimension that matters:

DimensionStoreKit 2 (Native)RevenueCat
Setup complexityManual — you write product loading, purchase flow, transaction listener, entitlement logic, and restore handling yourselfSDK + dashboard — install the package, call configure(), products come from the RC dashboard
Server-side validationDIY — you need to verify JWS tokens on your server or trust on-device verificationFully handled — RC validates every transaction server-side automatically
Cross-platformApple platforms only (iOS, macOS, watchOS, tvOS, visionOS)iOS, Android, Web (Stripe), React Native, Flutter, Unity
AnalyticsApp Store Connect only — delayed data, limited cohort viewsReal-time dashboard — MRR, churn, cohorts, LTV, trial conversion charts
A/B testing paywallsNot built in — you would need to build your own experiment frameworkBuilt-in Experiments with statistical significance reporting
PricingCompletely free — it is Apple's own frameworkFree up to $2,500 MTR, then $120+/month (1% of MTR on Scale plan)
Vendor lock-inNone — you own every line of codeMild — your entitlement logic and product config live in RC dashboard
Receipt validationAutomatic on-device JWS verification (iOS 15+)Automatic server-side verification
Subscription status APITransaction.currentEntitlements / Product.SubscriptionInfo.statusCustomerInfo.entitlements — unified across platforms
Promo offers / offer codesManual — generate signatures on your server, present via Purchase.promotionalOfferDashboard-managed — configure and target offers without code changes
Webhook supportApp Store Server Notifications V2 — you host the endpoint and parse signed payloadsRevenueCat webhooks — cleaner payload format, retry logic built in

StoreKit 2 Implementation Walkthrough

Let me walk through a complete StoreKit 2 subscription flow. This is the code you would write if you go fully native with zero third-party dependencies.

Product Loading

StoreKit 2 loads products using their App Store Connect product identifiers. You typically keep these in a constants file:

import StoreKit

enum SubscriptionProduct: String, CaseIterable {
    case monthlyPro = "com.yourapp.pro.monthly"
    case yearlyPro  = "com.yourapp.pro.yearly"
}

class SubscriptionManager: ObservableObject {
    @Published var products: [Product] = []
    @Published var purchasedProductIDs: Set<String> = []

    func loadProducts() async {
        do {
            let ids = SubscriptionProduct.allCases.map(\.rawValue)
            let storeProducts = try await Product.products(for: Set(ids))
            // Sort so monthly appears before yearly
            self.products = storeProducts.sorted {
                $0.price < $1.price
            }
        } catch {
            print("Failed to load products: \(error)")
        }
    }
}

Purchase Flow

Purchasing is a single async call. StoreKit 2 returns a strongly-typed Product.PurchaseResult so you can handle every outcome:

func purchase(_ product: Product) async throws -> Bool {
    let result = try await product.purchase()

    switch result {
    case .success(let verification):
        // Verify the transaction
        let transaction = try checkVerified(verification)
        // Deliver content to the user
        await updatePurchasedProducts()
        // Always finish the transaction
        await transaction.finish()
        return true

    case .userCancelled:
        return false

    case .pending:
        // Transaction requires approval (Ask to Buy, SCA)
        return false

    @unknown default:
        return false
    }
}

private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    switch result {
    case .unverified(_, let error):
        throw error
    case .verified(let safe):
        return safe
    }
}

Transaction Listener

This is the piece many tutorials skip — and it is critical. You need a long-running task that listens for transaction updates. These fire when a subscription renews in the background, when a family member shares a purchase, or when a refund is processed:

private var transactionListener: Task<Void, Error>?

func startTransactionListener() {
    transactionListener = Task.detached {
        for await result in Transaction.updates {
            do {
                let transaction = try self.checkVerified(result)
                await self.updatePurchasedProducts()
                await transaction.finish()
            } catch {
                print("Transaction verification failed: \(error)")
            }
        }
    }
}

deinit {
    transactionListener?.cancel()
}

Entitlement Checking

To check whether the user currently has an active subscription, iterate over Transaction.currentEntitlements:

func updatePurchasedProducts() async {
    var purchased: Set<String> = []

    for await result in Transaction.currentEntitlements {
        if case .verified(let transaction) = result {
            if transaction.revocationDate == nil {
                purchased.insert(transaction.productID)
            }
        }
    }

    await MainActor.run {
        self.purchasedProductIDs = purchased
    }
}

var isPro: Bool {
    !purchasedProductIDs.isEmpty
}

Restore Purchases

StoreKit 2 makes restore straightforward. Calling AppStore.sync() forces a sync with Apple's servers, then your entitlement check picks up any restored transactions:

func restorePurchases() async throws {
    try await AppStore.sync()
    await updatePurchasedProducts()
}

That is roughly 120 lines of code for a complete, working subscription system. Not trivial, but not overwhelming either.

RevenueCat Implementation Walkthrough

Now let me show the same functionality implemented with RevenueCat. The difference in verbosity is immediately obvious.

SDK Setup

After adding RevenueCat/purchases-ios via Swift Package Manager, configure it at app launch:

import RevenueCat

@main
struct MyApp: App {
    init() {
        Purchases.logLevel = .debug  // Remove in production
        Purchases.configure(
            withAPIKey: "appl_YOUR_REVENUECAT_API_KEY"
        )
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Displaying Offerings

RevenueCat groups your products into Offerings — a concept that lets you change which products are shown without an app update. Here is how you fetch and display them:

class PaywallViewModel: ObservableObject {
    @Published var offerings: Offerings?
    @Published var isPro: Bool = false

    func loadOfferings() async {
        do {
            self.offerings = try await Purchases.shared.offerings()
        } catch {
            print("Failed to fetch offerings: \(error)")
        }
    }
}

// In your SwiftUI view:
struct PaywallView: View {
    @StateObject private var vm = PaywallViewModel()

    var body: some View {
        VStack {
            if let packages = vm.offerings?.current?.availablePackages {
                ForEach(packages, id: \.identifier) { package in
                    PackageCell(package: package) {
                        Task { await purchase(package) }
                    }
                }
            }
        }
        .task { await vm.loadOfferings() }
    }
}

Making a Purchase

One method call. RevenueCat handles verification, finishing the transaction, and updating the customer info:

func purchase(_ package: Package) async {
    do {
        let result = try await Purchases.shared.purchase(package: package)
        // result.customerInfo has the updated entitlements
        self.isPro = result.customerInfo
            .entitlements["pro"]?.isActive == true
    } catch {
        print("Purchase failed: \(error)")
    }
}

Checking Entitlements

At any point in your app, you can check whether the user has access to premium features:

func checkEntitlements() async {
    do {
        let customerInfo = try await Purchases.shared.customerInfo()
        self.isPro = customerInfo
            .entitlements["pro"]?.isActive == true
    } catch {
        print("Failed to get customer info: \(error)")
    }
}

That is the entire implementation. Roughly 60 lines versus 120 for raw StoreKit 2. No transaction listener needed — RevenueCat handles that internally. No manual verification — done server-side. No restore button code — Purchases.shared.restorePurchases() is a single line.

Side-by-Side Code Comparison

Let me put the same operation — checking if a user is a paying subscriber — next to each other:

OperationStoreKit 2RevenueCat
Check if subscribedfor await result in Transaction.currentEntitlements — loop, verify each, check revocationcustomerInfo.entitlements["pro"]?.isActive — single boolean
Purchase a productCall product.purchase(), switch on result, verify, finish transaction, update local stateCall Purchases.shared.purchase(package:) — returns updated CustomerInfo
Listen for renewalsLong-running Task iterating Transaction.updatesHandled internally by the SDK — no code needed
Restore purchasestry await AppStore.sync() then re-check entitlementstry await Purchases.shared.restorePurchases()
Total lines of code~120 lines for a full implementation~60 lines for a full implementation

When to Use StoreKit 2 Alone

Going pure StoreKit 2 makes sense in specific scenarios. Here is when I recommend it:

  • You only target Apple platforms. If your app will never leave iOS/macOS, you do not need RevenueCat's cross-platform abstraction. StoreKit 2 gives you everything you need natively.
  • You want zero third-party dependencies. Some developers have a strict policy against external SDKs — especially in categories like health, finance, or enterprise where every dependency is a security audit item. StoreKit 2 means your subscription stack is pure Apple.
  • You have a simple subscription model. If you offer one or two products (say a monthly and yearly plan) with no complex offering logic, A/B testing, or promo offer campaigns, StoreKit 2's simplicity is actually a feature. Less abstraction means fewer things to go wrong.
  • You enjoy building infrastructure. If you are the kind of developer who wants to understand every byte flowing between your app and Apple's servers, StoreKit 2 gives you that transparency. You learn the system deeply, and that knowledge compounds.
  • You are optimizing for cost at scale. At 100K subscribers with $10/month ARPU, RevenueCat's 1% fee on the Scale plan is $10,000/month. StoreKit 2 is free. That difference can fund an entire engineering hire.

When to Use RevenueCat

RevenueCat shines when you need more than just purchase handling. Here is when it is the right call:

  • You need analytics beyond App Store Connect. App Store Connect's financial reports are delayed by 24-48 hours and lack cohort analysis. If you want real-time MRR tracking, trial-to-paid conversion funnels, and churn analysis, RevenueCat's dashboard is genuinely excellent. I check mine daily.
  • You might go cross-platform. Even if you are iOS-only today, if there is a non-zero chance of an Android or web version, RevenueCat gives you a unified subscriber identity and entitlement system that works everywhere. Retrofitting this later is painful.
  • You want to A/B test paywalls. RevenueCat Experiments let you test different product configurations, pricing, and trial lengths. I have seen 30-40% conversion lifts from paywall experiments. You cannot replicate this easily with StoreKit 2 alone.
  • You have multiple products or complex offerings. If you have monthly, yearly, lifetime, family plans, or region-specific pricing — managing all of that through RevenueCat's Offerings system is dramatically simpler than hardcoding product IDs.
  • You value speed over control. RevenueCat cuts your subscription implementation time in half. For indie developers shipping solo, that saved week can mean the difference between launching this month or next month.
  • You need webhooks for your backend. If your server needs to know when a user subscribes, cancels, or gets a refund, RevenueCat's webhooks are cleaner and more reliable than parsing App Store Server Notifications V2 yourself.

The Hybrid Approach

Here is something many developers do not realize: RevenueCat uses StoreKit 2 under the hood on iOS 15+ devices. You are not choosing between them in a mutually exclusive way. When you use RevenueCat, you get StoreKit 2's native purchase sheet, JWS verification, and subscription management APIs — RevenueCat just adds its management layer on top.

This means you can use both. Some developers use RevenueCat for purchase handling and analytics while also reading Transaction.currentEntitlements directly for fast, offline entitlement checks. Others use RevenueCat's Offerings for remote product configuration while building their own custom paywall UI.

The hybrid approach gives you the best of both worlds: RevenueCat's analytics and infrastructure with the ability to drop down to StoreKit 2 whenever you need fine-grained control. It is how The Swift Kit is architected — RevenueCat for the heavy lifting, with StoreKit 2 hooks available for customization.

Pricing at Scale

Cost is a real consideration, especially as your app grows. Here is what each approach costs at different subscriber counts, assuming $9.99/month average revenue per subscriber:

SubscribersMonthly RevenueStoreKit 2 CostRevenueCat CostRC Plan
250$2,498$0$0Free tier (under $2,500 MTR)
1,000$9,990$0$120/moGrow ($120/mo flat + overage)
10,000$99,900$0~$999/moScale (1% of MTR)
100,000$999,000$0~$9,990/moScale (1% of MTR) or Enterprise (custom)

A few things jump out from this table. First, RevenueCat is genuinely free for most indie developers. If you are under $2,500/month in tracked revenue — which covers the vast majority of apps in their first year — you pay nothing. The free tier includes full analytics, webhooks, and experiments.

Second, the 1% fee on the Scale plan is reasonable when you consider what you get. At $99,900/month in revenue, $999 for real-time analytics, cross-platform subscriber management, A/B testing, and webhook infrastructure is honestly a bargain compared to building and maintaining all of that yourself.

Third, if you reach 100K subscribers and $1M/month in revenue, you are in a position to negotiate an Enterprise deal — or to hire a team to build a custom StoreKit 2 backend. At that scale, you have options. Do not optimize for scale problems you do not have yet.

What About Alternatives?

RevenueCat is not the only StoreKit wrapper. Adapty, Qonversion, and Superwall all compete in this space. Adapty offers similar analytics with a slightly different pricing model. Qonversion targets the enterprise segment. Superwall focuses specifically on paywall experimentation. I have used Adapty on one project and it was solid, but RevenueCat's documentation, community, and ecosystem integrations are significantly ahead. For most indie developers, RevenueCat remains the default recommendation.

If you want a deeper dive into monetization tools, check out my guide on monetizing your iOS app.

My Personal Recommendation

After shipping subscription apps with both approaches, here is my honest take:

Start with RevenueCat. It is free for your first $2,500/month, it takes 30 minutes to integrate, and the analytics alone are worth it. If you outgrow it or need to eliminate the dependency later, migrating to pure StoreKit 2 is straightforward because RevenueCat uses StoreKit 2 under the hood — your products, pricing, and App Store Connect configuration do not change.

The only scenario where I recommend starting with raw StoreKit 2 is if you have a strong technical reason to avoid third-party SDKs (enterprise compliance, extreme privacy requirements, or simply a philosophical preference for zero dependencies). In that case, StoreKit 2 in 2026 is more than capable.

For everyone else — and especially for indie developers trying to validate an idea quickly — RevenueCat is the right default. Ship first, optimize later.

How The Swift Kit Handles Subscriptions

The Swift Kit uses RevenueCat with StoreKit 2 under the hood — exactly the hybrid approach I described above. When you clone the project, you get:

  • RevenueCat pre-configured with a clean SubscriptionManager class
  • Three production-ready paywall templates that display RevenueCat Offerings
  • Entitlement gating throughout the app — just define which features are premium
  • Restore purchases flow, trial logic, and promotional offer support
  • All wired to Supabase so your backend knows subscriber status via webhooks

You paste your RevenueCat API key, set up your products in App Store Connect and the RC dashboard, and subscriptions are live. No need to write any of the code from this post yourself. Check out the pricing — it is a one-time purchase with lifetime updates.

Wrapping Up

StoreKit 2 and RevenueCat are not enemies — they are layers. StoreKit 2 is the foundation that talks to Apple's payment system. RevenueCat is the management layer that adds analytics, cross-platform support, and experimentation on top. You can use either alone, or both together.

For most indie developers in 2026, the RevenueCat + StoreKit 2 combo is the pragmatic choice: fast to implement, free to start, and scales with your business. When the day comes that you need to optimize away RevenueCat's fee, you will have the revenue to fund that engineering effort — and that is a great problem to have.

Whatever you choose, stop deliberating and start building. The best subscription framework is the one that lets you ship your app this week.

Share this article

Ready to ship your iOS app faster?

The Swift Kit gives you a production-ready SwiftUI codebase with onboarding, paywalls, auth, AI integrations, and more. Stop building boilerplate. Start building your product.

Get The Swift Kit