Apple's StoreKit 2 examples tend to be written for teams that have a backend to validate receipts against and a server-side subscription-status source of truth. If you're a solo developer shipping a handful of small apps, that entire layer is usually unnecessary — StoreKit 2 was designed to let the client be the source of truth safely, and most of the "just use RevenueCat" advice skips over how solid the built-in tools actually are once you understand them.
Transaction.currentEntitlements is the whole trick
The single most useful API in StoreKit 2 is Transaction.currentEntitlements, an async sequence that yields every transaction the current user has an active, unexpired entitlement for — already verified, already deduplicated across devices via App Store server state. You don't reconstruct subscription status from a purchase event you cached; you just ask the App Store what's currently true.
func currentSubscriptionStatus() async -> Bool {
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else { continue }
if transaction.productID == "com.example.pro.monthly" {
return true
}
}
return false
}
Call this on launch and whenever you need a fresh read — it's cheap, it's local where possible, and it already accounts for expiration, refunds, and family sharing without you writing any of that logic yourself.
Listen for transaction updates, don't poll
Alongside currentEntitlements, StoreKit 2 gives you Transaction.updates, a long-lived async sequence that fires whenever a transaction changes state — a renewal, a refund, a family-sharing grant, a purchase made on another device. Start listening for this as early as possible, ideally before your first UI even renders, so you never miss a state change that happens while your app is in the background.
let updateTask = Task.detached {
for await result in Transaction.updates {
guard case .verified(let transaction) = result else { continue }
await transaction.finish()
await refreshEntitlements()
}
}
This listener is also where refunds show up. A support ticket like "I was charged but the app still says I'm not subscribed" almost always turns out to be a missed transaction update rather than a StoreKit bug — the fix is making sure this task is alive for the entire lifetime of the app, not just while a particular screen is on screen.
Offer codes and introductory pricing don't need a promo server
For solo apps running occasional discounts, it's tempting to reach for a server that mints signed promotional offers. For most cases you don't need one — App Store Connect lets you create offer codes directly, generate redeemable links, and StoreKit's own .redeemCodes() presentation handles the whole flow through Apple's UI. You get the discount logic, the redemption UI, and the fraud prevention for free, and your app never sees a raw price or discount percentage it has to enforce itself.
The pattern across all of this is the same one that shows up everywhere in local-first iOS development: the platform already solved the hard, security-sensitive part of the problem. The job left for you is reading its state correctly and reacting to it — not rebuilding a smaller, buggier version of App Store server infrastructure on your own backend.
Top comments (0)