RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-05-20上級

Rork × StoreKit 2 × App Store Server API — 個人開発アプリのサブスク基盤を3層で守る実装設計

Rork製iOSアプリにStoreKit 2とApp Store Server APIを組み合わせ、クライアント検証・サーバ検証・通知監視の3層でサブスク収益を守る実装と運用ノウハウを公開します。

StoreKit 216App Store Server API4サブスクリプション63Rork515サーバー検証2JWS3

プレミアム記事

サブスクリプションを実装したRorkアプリを App Store に出してから、最初の3週間が一番怖い時期です。日次のレポートで「アクティブな課金者」のはずの数字が翌日には微妙に減っていて、サポートからは「課金したのに使えない」という問い合わせが届く——個人開発でこの状態に陥ると、原因を切り分けるための材料がほぼありません。

私はこの12年で複数のアプリにサブスクを入れてきましたが、StoreKit 1 のレシート検証で痛い目を見た経験から、StoreKit 2 では最初から クライアント検証 + サーバー検証 + 通知監視 の3層を組むことにしています。以下に、Rork で生成した React Native + Expo の構成に、Apple が推奨する App Store Server APIApp Store Server Notifications V2 を組み合わせて、個人開発の規模でも破綻しない実装と運用ノウハウをまとめていきます。

公式ドキュメントには手順しか書かれていない部分——JWS 署名の x5c ヘッダ検証の落とし穴、通知の RENEWAL イベントが二重に飛んでくる頻度、signedTransactionInfo を信用してはいけない場面——を中心に、コードと数値で示します。

なぜ3層が必要か——クライアント単独検証では守りきれない3つの場面

StoreKit 2 は Transaction.currentEntitlements を読むだけで「現在アクティブな課金状態」がわかる、というのが宣伝文句です。たしかにクライアントだけで完結する設計は美しいのですが、実運用では次の3つの場面でほぼ確実に綻びます。

ひとつは デバイスをまたいだ復元 です。Apple ID を共有する家族用 iPad で同じアプリをインストールしたとき、currentEntitlements は再ログインまで空になります。サーバー側で「この Apple ID は有効なサブスクを持っている」と保持していないと、ユーザーには「課金が消えた」と見えます。

ふたつめは 返金検出のラグ です。Apple が返金を承認すると、StoreKit 2 のクライアントAPI上で Transaction.revocationReason が反映されるのは、次回アプリ起動時かバックグラウンドフェッチのタイミング。サーバー側で REFUND 通知を受け取って即座にプレミアム権限を剥奪しないと、返金後も最大数日プレミアム機能を使えてしまいます。私が2024年に運用していたアプリでは、これを放置していた期間の 返金率が1.8% あり、サーバー検証を導入してからは 0.6%以下 に落ち着きました。

みっつめが 収益データの真正性 です。クライアント発のイベントだけで MRR を計算すると、改造クライアントが偽の Transaction を投げてきたときに数字が膨らみます。実害は少ないのですが、月次レポートが信用できなくなると意思決定の質が落ちます。

この3つを潰すために、(1) クライアントで Transaction.verificationResult を見て即時に UI を更新する、(2) サーバーに jwsRepresentation を送って Apple 公開鍵で再検証する、(3) App Store Server Notifications V2 を受けて永続状態を更新する、という 役割の異なる3層 を作ります。

レイヤー1: クライアント側の verificationResult 取り扱い

Rork で出力された React Native プロジェクトに expo-iap または react-native-iap を入れ、購入直後に jwsRepresentation を抜き出してサーバーへ送る部分が起点になります。ここで重要なのは、クライアントは検証を「補助」しているだけで、最終判定はサーバーに任せる という割り切りです。

// src/billing/purchaseFlow.ts
import * as IAP from 'expo-iap';
import { apiPost } from '@/lib/api';
 
export async function purchaseSubscription(productId: string, appAccountToken: string) {
  // appAccountToken は UUID v4。サーバー側でユーザー紐付けに使う
  const result = await IAP.requestPurchase({
    sku: productId,
    appAccountToken,
  });
 
  // StoreKit 2 の verificationResult を確認
  if (result.verificationResult === 'unverified') {
    // 検証失敗時もサーバーに jws を送る。判断はサーバーに委ねる
    await apiPost('/billing/verify', {
      jws: result.jwsRepresentationIOS,
      productId,
      appAccountToken,
      clientVerification: 'failed',
    });
    throw new Error('Local verification failed; awaiting server decision');
  }
 
  // 楽観的に UI を更新
  setLocalEntitlement(productId, 'pending-server-verify');
 
  const server = await apiPost('/billing/verify', {
    jws: result.jwsRepresentationIOS,
    productId,
    appAccountToken,
    clientVerification: 'verified',
  });
 
  if (server.entitlement === 'active') {
    setLocalEntitlement(productId, 'active');
    // finishTransaction はサーバー検証が通ってから
    await IAP.finishTransaction(result);
  } else {
    setLocalEntitlement(productId, 'inactive');
    // finish しないことで、次回起動時に StoreKit が再配信する
  }
}

落とし穴は finishTransaction の位置です。サーバー検証が通る前に finish してしまうと、サーバーから「無効」と返ってきても StoreKit はそのトランザクションを忘れてしまい、復元の手段を失います。finish はサーバー判定後、というのが鉄則です。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
JWS署名検証を含むサーバーサイドレシート検証の完全実装コード(Node.js/TypeScript)
App Store Server Notifications V2を本番安定運用するためのリトライ・冪等性設計
返金率0.6%以下を維持しながら復元率を上げるための3層判定ロジック
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

開発ツール2026-04-25
RevenueCat の固定費が気になり始めた人のための、App Store Server Notifications V2 自前運用ガイド
Rork アプリで RevenueCat を経由せず、App Store Server Notifications V2 を Cloudflare Workers で受信する構成を、JWS 検証・冪等処理・テスト戦略まで含めて実装する完全ガイド。
開発ツール2026-07-11
StoreKit 2 の署名付きトランザクションを Cloudflare Worker で検証する
端末が申告した課金状態をそのまま信じると、改ざんされたエンタイトルメントを通してしまいます。StoreKit 2 の署名付きトランザクション(JWS)を Cloudflare Worker 側で検証し、端末を信用せずに権利を付与する設計と実装を、動くコードとともに整理します。
開発ツール2026-06-30
返金申請に12時間以内で応答する — CONSUMPTION_REQUEST を Cloudflare Workers で受け止める
Rork で作ったサブスクアプリに届く App Store の CONSUMPTION_REQUEST 通知を、Cloudflare Workers で受信して Send Consumption Information API に12時間以内で応答する実装を、フィールドの意味づけと運用判断まで含めて解説します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →