RORK LABJP
MAX — Rork Max bills itself as the first web Swift app builder, publishing to the App Store in two clicks with no Xcode requiredAPPLE — It generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, and Vision ProEXPO — The standard tier builds native iOS and Android apps on React Native (Expo) from a plain-English descriptionFUNDING — Rork raised $2.8M from a16z, strengthening its position in AI no-code mobile developmentPRICE — Free to start, with paid plans from $25/month — an accessible entry point for solo developersWWDC — WWDC 2026 pushes Apple Intelligence forward, raising the value of native features and widening AI integration options for no-code appsMAX — Rork Max bills itself as the first web Swift app builder, publishing to the App Store in two clicks with no Xcode requiredAPPLE — It generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, and Vision ProEXPO — The standard tier builds native iOS and Android apps on React Native (Expo) from a plain-English descriptionFUNDING — Rork raised $2.8M from a16z, strengthening its position in AI no-code mobile developmentPRICE — Free to start, with paid plans from $25/month — an accessible entry point for solo developersWWDC — WWDC 2026 pushes Apple Intelligence forward, raising the value of native features and widening AI integration options for no-code apps
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 212Subscriptions10Monetization29App Store Server API3Rork395iOS77

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-06-13
Raising Your Subscription Price: How to Handle Existing Subscribers on the App Store and Google Play
Raise your subscription price without losing existing subscribers: break-even churn math, store consent flows, PRICE_INCREASE and EXPIRED handling, and cohort tracking.
Business2026-05-05
Rork Max × RevenueCat Paywalls SDK: Remote Paywalls, A/B Testing & Conversion Optimization Complete Guide
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-05-02
Rork × Subscription Groups and Intro Offers — Implementation Patterns That Lift Subscription Revenue
If you shipped a monthly subscription with your Rork-built app and watched first-month churn climb past 50%, the fix usually lives in two places: how your Subscription Group is structured, and which intro offer format you picked. This guide walks through both, with production-ready StoreKit 2 code.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →