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.RenewalStatefor cleaner subscription status checks and theMessageAPI for price-increase consent. - iOS 17 brought
Transaction.offer(for:)for programmatic offer codes, plus theSubscriptionStoreView— Apple's own SwiftUI paywall component. - iOS 18 introduced the Subscription Status API v2 with richer renewal info, improved
StoreKit Testingwith simulated server notifications, and the ability to readTransaction.environmentto 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:
| Dimension | StoreKit 2 (Native) | RevenueCat |
|---|---|---|
| Setup complexity | Manual — you write product loading, purchase flow, transaction listener, entitlement logic, and restore handling yourself | SDK + dashboard — install the package, call configure(), products come from the RC dashboard |
| Server-side validation | DIY — you need to verify JWS tokens on your server or trust on-device verification | Fully handled — RC validates every transaction server-side automatically |
| Cross-platform | Apple platforms only (iOS, macOS, watchOS, tvOS, visionOS) | iOS, Android, Web (Stripe), React Native, Flutter, Unity |
| Analytics | App Store Connect only — delayed data, limited cohort views | Real-time dashboard — MRR, churn, cohorts, LTV, trial conversion charts |
| A/B testing paywalls | Not built in — you would need to build your own experiment framework | Built-in Experiments with statistical significance reporting |
| Pricing | Completely free — it is Apple's own framework | Free up to $2,500 MTR, then $120+/month (1% of MTR on Scale plan) |
| Vendor lock-in | None — you own every line of code | Mild — your entitlement logic and product config live in RC dashboard |
| Receipt validation | Automatic on-device JWS verification (iOS 15+) | Automatic server-side verification |
| Subscription status API | Transaction.currentEntitlements / Product.SubscriptionInfo.status | CustomerInfo.entitlements — unified across platforms |
| Promo offers / offer codes | Manual — generate signatures on your server, present via Purchase.promotionalOffer | Dashboard-managed — configure and target offers without code changes |
| Webhook support | App Store Server Notifications V2 — you host the endpoint and parse signed payloads | RevenueCat 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:
| Operation | StoreKit 2 | RevenueCat |
|---|---|---|
| Check if subscribed | for await result in Transaction.currentEntitlements — loop, verify each, check revocation | customerInfo.entitlements["pro"]?.isActive — single boolean |
| Purchase a product | Call product.purchase(), switch on result, verify, finish transaction, update local state | Call Purchases.shared.purchase(package:) — returns updated CustomerInfo |
| Listen for renewals | Long-running Task iterating Transaction.updates | Handled internally by the SDK — no code needed |
| Restore purchases | try await AppStore.sync() then re-check entitlements | try 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:
| Subscribers | Monthly Revenue | StoreKit 2 Cost | RevenueCat Cost | RC Plan |
|---|---|---|---|---|
| 250 | $2,498 | $0 | $0 | Free tier (under $2,500 MTR) |
| 1,000 | $9,990 | $0 | $120/mo | Grow ($120/mo flat + overage) |
| 10,000 | $99,900 | $0 | ~$999/mo | Scale (1% of MTR) |
| 100,000 | $999,000 | $0 | ~$9,990/mo | Scale (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
SubscriptionManagerclass - 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.