◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Business
⟐ Business/2026-06-14Advanced

Validating StoreKit 2 Subscriptions Server-Side: Granting Access Without Trusting the Device

To stop 'I paid but the feature won't unlock' and 'still usable after canceling,' you need a design that does not trust the device's verdict and settles entitlements on the server. Covers StoreKit 2 signed transactions, verification with the App Store Server API, and state sync via App Store Server Notifications V2, from real indie monetization.

StoreKit 217Subscriptions17Monetization40App Store Server API4Rork611iOS118

✦ Premium Article

When I first added subscriptions as an indie developer, the first thing I built was the naive version: "when a purchase succeeds, set a Pro flag on the device." It works fine right after purchase, but it falls apart the moment you enter the real world of cancellations, refunds, and signing in on another device. If you judge entitlement only inside the device, the device of a user who has canceled keeps insisting "I'm Pro."

Precisely because this part drives revenue, the source of truth for the verdict belongs on the server. That was the starting point of subscription design in the StoreKit 2 era. Unlike AdMob ad revenue, a subscription has to correctly answer "do you have access right now" every single time. Build this loosely and even if you raise the conversion rate, post-cancel freeloading quietly eats into your real LTV.

Why not trust the device's verdict

StoreKit 2 lets you read current access on the device via Transaction.currentEntitlements. It is self-contained and convenient, but relying on it alone leaves two holes.

One is sync across devices and reinstalls. When a user switches phones, the new device does not know the entitlement until it restores purchase history. The other is tampering and offline continuation. If you unlock from a local flag alone, a canceled user keeps access as long as they simply never reopen the app.

Put the source of truth on the server and you can return the same answer from any device, and reflect cancellations and refunds instantly, server-driven. I make the server's response the final word for any revenue-related entitlement decision.

Send the signed transaction to the server

StoreKit 2 transactions are signed as a JWS (JSON Web Signature). The device sends that signed string as-is to the server, and the server trusts the contents only after verifying the signature with Apple's public key. The crux is verifying the signature, not trusting raw purchase JSON.

import StoreKit
 
func syncPurchases() async {
    for await result in Transaction.currentEntitlements {
        guard case .verified(let transaction) = result else { continue }
        // jwsRepresentation is the signed string. Send it as-is to the server
        await postToServer(signedTransaction: result.jwsRepresentation,
                           productID: transaction.productID)
    }
}
 
func purchase(_ product: Product) async throws {
    let result = try await product.purchase()
    if case .success(let verification) = result,
       case .verified(let transaction) = verification {
        await postToServer(signedTransaction: verification.jwsRepresentation,
                           productID: transaction.productID)
        await transaction.finish()
    }
}

Call transaction.finish() only after you have sent it to the server and confirmed the entitlement. Finish before sending and a dropped network connection loses the transaction, leaving the user paid but without access.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Follow the full flow of verifying a signed transaction on the server and granting the user access, in working Swift and Node.js
✦Take home a state-sync table design that reflects DID_RENEW, EXPIRED and REFUND from App Store Server Notifications V2 into entitlement state
✦Learn to close two gaps that hit both revenue and trust: 'Pro still opens after canceling' and 'still usable after a refund'
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⟐ Business2026-05-05
Rork Max × RevenueCat Paywalls SDK: Remote Paywalls, A/B Testing, and Conversion Gains
A complete guide to integrating RevenueCat Paywalls SDK with Rork Max apps — enabling remote paywall updates and A/B testing without App Store reviews, with production-ready code examples and conversion optimization strategies.
⟐ Business2026-04-04
Rork App Revenue Design: Where Ads and Subscriptions Eat Each Other
How to layer AdMob, subscriptions, and one-time purchases in a Rork app without letting them cannibalize each other. Covers value-density placement, entitlement-first RevenueCat architecture, server-side receipt verification, and behavior-triggered paywalls.
⬡ Dev Tools2026-04-28
Why In-App Purchases Disappear with Family Sharing — Building Bulletproof IAP for Family Apps with Rork
Subscription revenue from Family Sharing purchases can vanish days after the sale settles. This guide covers the four refund triggers, a StoreKit 2 + Server Notifications V2 implementation for Rork apps, and a StoreKit Testing matrix that catches family edge cases before launch.