◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-05-20上級

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

Rork製iOSアプリのサブスク収益を、StoreKit 2のクライアント検証、App Store Server APIによるサーバー側JWS検証、Server Notifications V2の監視という3層で守る実装設計です。権利テーブルの設計、定期再同期、サンドボックスの罠、障害時の運用まで公開します。

StoreKit 217App Store Server API4サブスクリプション66Rork585サーバー検証JWS3

✦ プレミアム記事

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

私はこの12年で複数のアプリにサブスクを入れてきましたが、StoreKit 1 のレシート検証で痛い目を見た経験から、StoreKit 2 では最初から クライアント検証 + サーバー検証 + 通知監視 の3層を組むことにしています。以下に、Rork で生成した React Native + Expo の構成に、Apple が推奨する App Store Server API と App 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-04-28
Family Sharing で IAP が消える4つのトリガー — Rork で子ども向けアプリの課金を安全に設計する
Family Sharing 経由の購入で IAP 収益が突然消える原因と、Rork で実装する StoreKit 2・Server Notifications V2 を組み合わせた安全な課金設計を、実コードとテスト手順まで網羅的に解説します。
⬡ 開発ツール2026-10-15
EXPO_PUBLIC_ の値が、開発では読めて本番だけ undefined になる書き方を、変換結果で見分けます
API の URL が開発中は読めるのに、ビルドした途端に undefined になる。原因は process.env の書き方でした。babel-preset-expo の変換結果を開発・本番で並べ、置き換わる書き方と残ってしまう書き方を表にしました。