2025年2月19日·1分で読めます

アップグレードとアドオンに対応するプランとエンタイトルメントのデータベーススキーマ

アップグレード、アドオン、トライアル、取り消しに対応するプランとエンタイトルメントのデータベーススキーマ。ハードコードされたルールを使わず、明確なテーブルとチェックで運用可能にします。

アップグレードとアドオンに対応するプランとエンタイトルメントのデータベーススキーマ

なぜプランと機能はすぐにややこしくなるのか

価格ページではプランはシンプルに見えます:Basic、Pro、Enterprise。問題が始まるのは、それらの名前をアプリ内の実際のアクセスルールに落とし込もうとした瞬間です。

if plan = Pro then allow X のようなハードコードされた機能チェックは最初のバージョンでは動きます。しかし価格が変わると問題が出ます。ある機能がProからBasicへ移動したり、新しいアドオンが出現したり、営業の契約でカスタムバンドルが入ったりします。すると同じルールがAPI、UI、モバイル、バッチ処理のあちこちにコピーされ、ひとつ直して別の場所を忘れるとユーザーが気付きます。

二つ目の問題は時間です。サブスクリプションは静的なラベルではありません。期間の途中でアップグレードしたり、翌月にダウングレードしたり、一時停止や残日数のあるキャンセルが発生します。データベースが「現在のプラン」しか保存していないと、タイムラインを失い、後で基本的な質問にも答えられません:先週の火曜日に何にアクセスできていたのか?サポートがなぜ返金を承認したのか?

アドオンはさらに状況を複雑にします。アドオンは追加の席を解放したり、制限を緩和したり、特定の機能を有効にします。人はどのプランでも買えて、後で外すこともでき、ダウングレード後も保持することがあります。ルールがコードに埋め込まれていると、特別対応の山が増えていきます。

典型的に設計を破綻させる状況は次の通りです:

  • 期間途中のアップグレード:アクセスは即時に変わるべきで、課金の按分は別のルールを持つかもしれません。
  • 予定されたダウングレード:有料期間が終わるまでは高いアクセスを維持する場合があります。
  • グランドファザー(既存顧客優遇):古い顧客は新顧客が持たない機能を保持することがあります。
  • カスタム契約:同じプラン名でもあるアカウントだけがFeature Aを持ち、Feature Bは持たない場合があります。
  • 監査要件:サポートや経理、コンプライアンスが「いつ何が有効だったか」を問うことがあります。

目的は明確です:価格変更に合わせて柔軟に変化するアクセス制御モデル。ビジネスロジックを都度書き直すのではなく、1箇所で「彼らはこれができるか?」と問えること、そしてその答えを説明するデータの履歴を持つことです。

この記事の終わりまでに、コピーして使えるスキーマパターンを得られます。プランやアドオンを入力とし、エンタイトルメントが機能アクセスの単一の真実の源になります。このアプローチはAppMasterのようなノーコードビルダーにも適合します。ルールをデータに保持し、バックエンド、Web、モバイルから一貫して照会できるからです。

主要用語:プラン、アドオン、エンタイトルメント、アクセス

多くのサブスクリプション問題は語彙の混乱から始まります。誰もが同じ言葉を違う意味で使うと、スキーマは特別対応だらけになります。

プランとエンタイトルメントのデータベーススキーマで別々にしておくべき用語は次の通りです:

  • Plan(プラン): 誰かが購読したときにデフォルトで得るバンドル(例:Basic、Pro)。通常は基準となる上限や含まれる機能を設定します。
  • Add-on(アドオン): 基準を変更する任意の購入(例:「追加席」や「高度なレポート」)。プランを変えずに付け外し可能であるべきです。
  • Entitlement(エンタイトルメント): プラン + アドオン + オーバーライドを組み合わせて算出された「現在彼らが持っているもの」。アプリはこれを問い合わせるべきです。
  • Permission(許可、Capability): 具体的な操作(例:「データをエクスポートできる」「請求を管理できる」)。通常は役割とエンタイトルメントに依存します。
  • Access(アクセス): アプリがルールを適用したときの実際の結果(画面が機能を表示/非表示、API呼び出しが許可/拒否、制限が適用される等)。

フィーチャーフラグは関連しますが別物です。フィーチャーフラグはロールアウトや実験、障害時に機能をオフにするための製品側のスイッチです。エンタイトルメントは顧客固有のアクセスで、支払い状況や付与に基づきます。課金に紐づくアクセスを変えるならエンタイトルメントを使い、グループ単位で挙動を切り替えるならフラグを使いましょう。

スコープも混乱を生みます。次を明確に保ってください:

  • User(ユーザー): 一人の人。役割(管理者 vs メンバー)や個人制限に使います。
  • Account(アカウント/顧客): 支払い主体。請求情報とサブスクリプション所有権に適します。
  • Workspace(ワークスペース/プロジェクト/チーム): 実際に作業が行われる場所。席数、ストレージ、有効モジュールなどはここに紐づくことが多いです。

時間は重要です。アクセスは変わるため、モデル化しましょう:

  • 開始と終了: エンタイトルメントはウィンドウ内でのみ有効にできます(トライアル、プロモ、年契約)。
  • 予定変更: アップグレードは即時開始、ダウングレードは更新時に適用されることが多いです。
  • 猶予とキャンセル: 支払い失敗後に制限付きでアクセスを許すことがあるが、明確な終了日が必要です。

例:ある会社がProを利用中に「Advanced Reporting」アドオンを月中に追加し、次サイクルでBasicへダウングレードを予定したとします。プランはあとで変わり、アドオンは今から開始されます。エンタイトルメント層があれば「今日このワークスペースはAdvanced Reportingを使えるか?」という問いに一貫して答えられます。

プランと機能のためのシンプルなコアスキーマ

良いプランとエンタイトルメントのデータベーススキーマは小さく始めます:売るもの(プランとアドオン)を人ができること(機能)から分離する。これらを分けておけば、アップグレードや新しいアドオンはデータの変更になり、コードの書き換えではなくなります。

多くのサブスクリプション製品で実用的に機能するコアテーブル群は以下の通りです:

  • products: 売れるもの(Base plan、Team plan、Extra seats add-on、Priority support add-on)。
  • plans: 任意。プランを製品の特別なタイプとして扱いたい場合に使用(課金間隔、公開順などのプラン専用フィールド)。多くは products に格納し product_type カラムで区別します。
  • features: 機能カタログ(APIアクセス、max projects、export、SSO、SMSクレジット)。
  • product_features(または plan_features): どの製品がどの機能を含むかを示す結合テーブル。通常は値を持ちます。

この結合テーブルに力が集まります。機能は単なるオン/オフでないことが多いです。プランは max_projects = 10 を含むかもしれませんし、アドオンが +5 を追加することもあります。つまり product_features は少なくとも次をサポートすべきです:

  • feature_value(数値、テキスト、JSON、または別カラム)
  • value_type(boolean、integer、enum、json)
  • grant_mode(置換か加算か) — アドオンが基準を上書きではなく「+5席」できるように

アドオンも製品としてモデル化します。違いは購入の仕方だけです。基盤プラン製品は「1つだけ」として扱い、アドオンは数量を許す場合があります。しかし両方とも同じように機能へマッピングします。これにより「もしアドオンXなら機能Yを有効にする」というコード中の特別対応を避けられます。

機能はコード定数でなくデータとして扱うべきです。複数サービスで機能チェックをハードコードすると、やがて不整合が生じます(Webでは許可、モバイルでは拒否、バックエンドは別の判断)。機能がデータベースにあれば、アプリは一貫した質問を行い、行を編集するだけで変更を反映できます。

名前付けは予想以上に重要です。マーケティング名が変わっても変えない安定した識別子を使いましょう:

  • feature_key 例:max_projectsssopriority_support
  • product_code 例:plan_starter_monthlyaddon_extra_seats

表示ラベル(feature_nameproduct_name)は別に保持します。AppMasterのData DesignerでPostgreSQLを使うなら、これらのキーをユニークにしておくと連携やレポートが安定します。

エンタイトルメント層:"できるか?"を問う1箇所

多くのサブスクリプションシステムは「買ったもの」が一箇所にあり、「できること」の算出が別々のコードパスで行われると破綻します。対策はエンタイトルメント層:ある時点での主体の実効アクセスを表す単一のテーブル(またはビュー)です。

アップグレード、ダウングレード、トライアル、一時付与に耐えるスキーマを目指すなら、この層がすべてを予測可能にします。

実用的なエンタイトルメントテーブル

各行を1つのクレームとして考えてください:「この主体はこの機能をこの値で、いつからいつまで、どのソースで持っているか」。一般的な形は:

  • subject_type(例:"account"、"user"、"org")と subject_id
  • feature_id
  • value(その機能の実効値)
  • source("direct"、"plan"、"addon"、"default" など)
  • starts_at と ends_at(ongoingならends_atはNULL可)

値の実装方法は幾つかあります:テキスト/JSONカラムと value_type、または value_bool/value_int/value_text のように分ける方法。平易で問い合わせしやすく保つことが重要です。

ほとんどの製品をカバーする値の型

機能は単純なオン/オフだけではありません。一般的な値型は:

  • Boolean:有効/無効(can_export = true
  • Quota number:上限(seats = 10api_calls = 100000
  • Tier level:ランク(support_tier = 2
  • String:モードやバリアント(data_retention = "90_days"

優先順位:競合をどう解決するか

競合は普通に起こります。プランで5席、アドオンで10席を買い、サポートが手動で付与することもあります。

明確なルールを決め、それをどこでも守りましょう:

  1. 直接付与(direct)がプランを上書きする
  2. 次にアドオン
  3. 最後にデフォルト

シンプルな方法は候補行(プラン由来、アドオン由来、直接付与)をすべて保持し、ソースの優先度と最新のstarts_atでソートして各 subject_id + feature_id の最終“勝者”を決めることです。

具体例:顧客が今日ダウングレードしても、月末までは有効なアドオンをすでに支払っているなら、エンタイトルメントの starts_at/ends_at により、プラン由来の機能は即時に変わり、アドオン由来の機能は ends_at まで残ります。アプリは1つのクエリで「できるか?」に答えられます。

サブスクリプション、アイテム、時間制限付きアクセス

請求とアクセスを分離する
Stripe決済を接続し、請求ロジックではなくエンタイトルメントでアクセスを更新しましょう。
支払いを追加

プランカタログ(plans、add-ons、features)は“何を売るか”。サブスクリプションは“誰がいつ何を持つか”です。これらを分離すると、アップグレードやキャンセルが怖くなくなります。

実用的なパターンは:アカウントに1つのsubscription、そしてその下に複数のsubscription_items(ベースプラン用の1つと0個以上のアドオン)を持つことです。これによりタイムラインを変更履歴として記録でき、アクセスルールを差し替える必要がなくなります。

購入タイムラインをモデル化するコアテーブル

シンプルで問い合わせしやすい2つのテーブルで十分です:

  • subscriptions: id、account_id、status(active、trialing、canceled、past_due)、started_at、current_period_start、current_period_end、canceled_at(nullable)
  • subscription_items: id、subscription_id、item_type(plan、addon)、plan_id/addon_id、quantity、started_at、ends_at(nullable)、source(stripe、manual、promo)

各アイテムごとに日付を持たせることで、アドオンを30日だけ付与したり、キャンセル時に有料期間の終了までプランを維持したりできます。

課金とアクセスは分離する

按分、請求書、支払いリトライは課金の問題です。機能アクセスはエンタイトルメントの問題です。請求情報から直接「アクセス」を計算しようとしないでください。

代わりに、課金イベントがサブスクリプションレコードを更新する(例:current_period_end を延長、subscription_item 行を作成、ends_at を設定)ようにし、アプリはアクセスの質問を課金計算ではなくサブスクリプションタイムライン(後にエンタイトルメント層)から答えるようにします。

予定された変更を驚きなく処理する

アップグレードやダウングレードは特定の瞬間に効力を持つことが多いです:

  • サブスクリプションに pending_plan_idchange_at を追加して単一の予定変更を示す。
  • あるいは複数の将来変更や履歴が必要なら subscription_changes テーブル(subscription_id、effective_at、from_plan_id、to_plan_id、reason)を使う。

これにより「ダウングレードは期間末に起こる」といったルールをコードのあちこちにハードコードする必要がなくなります。スケジュールはデータです。

トライアルの扱い

トライアルは時間制限付きアクセスであり、ソースが異なるだけです。2つの選択肢がクリーンです:

  • トライアルをサブスクリプションのステータス(trialing)として扱い、trial_start/trial_endを持つ。
  • あるいは trial ソースの started_at/ends_at を持つsubscription_itemやentitlementを作る。

AppMasterで構築する場合、これらのテーブルはData Designerに自然に対応し、現在有効なものを特別扱いせずに取得できます。

ステップバイステップ:パターンを実装する

スキーマをテーブルに落とし込む
この記事のスキーマテーブルをAppMaster Data Designerで数分で設計できます。
データをモデル化

良いスキーマは1つの約束から始まります:機能ロジックは散在したコードではなくデータにある。アプリは「今の実効エンタイトルメントは何か?」という1つの質問をして明確な答えを得るべきです。

1) 安定したキーで機能を定義する

feature テーブルを作り、UIラベルが変わっても決して名前を変えない安定したキーを持たせます。良いキーは export_csvapi_calls_per_monthseats のようなものです。

値の扱い方(boolean/numberなど)を付けておくとシステムが値をどう扱うか分かりやすくなります。

2) プランとアドオンをエンタイトルメントにマップする

プランに何が含まれるか、各アドオンが何を付与するかの真実のソースを用意します。

実用的な手順例:

  • すべての機能を1つの feature テーブルに入れ、安定キーと値の型を持たせる。
  • planplan_entitlement を作り、各行で機能の値を付与する(例:seats = 5export_csv = true)。
  • addonaddon_entitlement を作り、追加の値を付与する(例:seats + 10api_calls_per_month + 50000priority_support = true)。
  • 値の合成ルールを決める:booleanは通常 OR、数値上限はMAX(大きい方)、席数のような加算はSUMなど。
  • エンタイトルメントの開始と終了を記録し、アップグレード、キャンセル、按分がアクセスチェックを壊さないようにする。

AppMasterで作るなら、これらのテーブルをData Designerでモデル化し、合成ルールを小さな「ポリシー」テーブルやenumとしてBusiness Processで扱うと良いでしょう。

3) “実効エンタイトルメント”を生成する

実行時に毎回計算するか、変更があったときにキャッシュスナップショットを生成するかの2択です。多くのアプリではスナップショットの方が理解しやすく、負荷に対して速くなります。

一般的には account_entitlement テーブルに機能ごとの最終結果と valid_from / valid_to を保存します。

4) 1つのチェックでアクセスを強制する

ルールを画面やエンドポイント、バッチに分散して配置しないでください。実効エンタイトルメントを読み取って判断する関数をアプリコードに1つだけ置きます。

can(account_id, feature_key, needed_value=1):
  ent = get_effective_entitlement(account_id, feature_key, now)
  if ent.type == "bool": return ent.value == true
  if ent.type == "number": return ent.value >= needed_value

すべてがこの can(...) を呼ぶようにすれば、アップグレードやアドオンはデータ更新で済み、コードの書き換えは不要になります。

例:アップグレードとアドオンが問題を起こさないシナリオ

6人のサポートチームがStarterプランを使っているとします。Starterには3席と月あたり1,000件のSMSが含まれます。月中に6人になり、さらに5,000件のSMSパックを追加したいとします。特別対応のコードを書かずにこれを機能させたいはずです。

Day 1: Starter開始

アカウントの subscription を作り、請求期間(例:1月1日〜1月31日)を設定します。次にプラン用の subscription_item を追加します。

チェックアウト時(または夜間ジョブで)、その期間のエンタイトルメント付与を書き込みます:

  • entitlement_grant: agent_seats, value 3, start Jan 1, end Jan 31
  • entitlement_grant: sms_messages, value 1000, start Jan 1, end Jan 31

アプリは「彼らはどのプランか?」ではなく「今の実効エンタイトルメントは何か?」を尋ね、seats = 3、SMS = 1000 を返します。

Day 15: Proへアップグレード、同日SMSパック追加

1月15日にProへアップグレード(10席、2,000 SMS含む)し、同日にSMSパックを追加します。古い付与を編集せず、新しいレコードを追加します:

  • 古いプランアイテム(Starter)の subscription_item の end を Jan 15 に設定
  • 新しいプランアイテム(Pro)を Jan 15 から Jan 31 で作成
  • 新しいアドオンアイテム(SMS Pack 5000)を Jan 15 から Jan 31 で作成

同じ期間の付与を追記します:

  • entitlement_grant: agent_seats, value 10, start Jan 15, end Jan 31
  • entitlement_grant: sms_messages, value 2000, start Jan 15, end Jan 31
  • entitlement_grant: sms_messages, value 5000, start Jan 15, end Jan 31

1月15日に何が起きるか?

  • 席数:実効席数は10になります(席数は "max を取る" などのルールを選ぶ)。その日に3人を追加できます。
  • SMS:残り期間の実効SMSは7,000になります(メッセージパックは加算するルール)。

既存の使用は移動する必要はありません。使用量テーブルはメッセージ送信をカウントし続け、エンタイトルメントチェックが現在の実効上限と比較するだけです。

Day 25: ダウングレードを予定、期間終了までは保持

1月25日に2月1日開始でStarterへのダウングレードを予定します。1月の付与は触りません。次の期間用に将来のアイテム(または将来の付与)を作成します:

  • subscription_item (Starter) start Feb 1, end Feb 28
  • 2月1日開始のSMSパックアイテムはなし

結果:1月31日まではProの席数とSMSパックを保持し、2月1日に実効席数は3に落ち、SMSはStarterの制限に戻ります。これは理解しやすく、AppMasterのノーコードワークフローにも自然にマッピングされます:日付を変えるだけで行が作られ、エンタイトルメントクエリは変わりません。

よくある間違いと落とし穴

価格変更を安全に管理する
コードに触らずに機能、プランマッピング、オーバーライドを管理できる管理画面を作りましょう。
管理画面を作る

ほとんどのサブスクリプションバグは課金バグではありません。製品内でロジックが散らばっていることによるアクセスバグです。設計を破綻させる最速の方法は「彼らはこれができるか?」を5箇所で答えることです。

典型的な失敗はUI、API、夜間ジョブそれぞれにルールをハードコードすることです。UIがボタンを隠すがAPIがブロックし忘れ、夜間ジョブは別のチェックをして動き続ける、という状況になります。結果的に「動くときと動かないときがある」レポートが増え、再現が難しくなります。

もうひとつの罠は plan_id チェックを使うことです。最初は単純に感じます(Plan Aはエクスポート可能、Plan Bは不可)が、アドオン、グランドファザー、トライアル、エンタープライズ例外が出ると崩れます。もし "if plan is Pro then allow..." と条件を書くなら、その迷路を永遠に保守することになります。

時間とキャンセルのエッジケース

単に has_export = true のようなブールだけを保存し日付を付けないとアクセスが「残る」か「早く消える」かの問題が起きます。キャンセル、返金、チャージバック、月途中のダウングレードには時間の境界が必要です。

避けるための簡単なチェック:

  • すべてのエンタイトルメント付与はソース(plan、add-on、manual override)と時間範囲を持つこと。
  • すべてのアクセス判断は「now が start と end の間か」を使う(null end の扱いは明確に)。
  • バックグラウンドジョブは実行時にエンタイトルメントを再チェックし、昨日の状態を前提にしない。

課金とアクセスを混ぜない

課金レコードとアクセスルールを同じテーブルに混ぜると問題になります。請求は請求書、税金、按分、プロバイダID、リトライ状態を必要とします。アクセスは明確な機能キーと時間ウィンドウを必要とします。混ぜると課金マイグレーションがプロダクト障害になります。

最後に、多くのシステムは監査トレイルを省きます。「なぜエクスポートできるのか?」と聞かれたら「Add-on X によって 2026-01-01 から 2026-02-01 の間有効化された」あるいは「サポートがチケット1842で手動付与した」などの答えが必要です。なければサポートとエンジニアは推測することになります。

AppMasterで構築する場合は、Data Designerのモデルに監査フィールドを入れ、can they? チェックをWeb、モバイル、スケジュール処理で使われる単一のBusiness Processにしましょう。

出荷前のチェックリスト

価格ページを越える価値を出す
ノーコードバックエンドとUIから本番対応のソースコードを生成します。
コードを生成

プランとエンタイトルメントのスキーマを出荷する前に、理論ではなく現実の問いで最終確認をしてください。目標はアクセスが説明可能で、テスト可能で、変更に強いことです。

現実確認の質問

1人のユーザーと1つの機能を選び、サポートや経理に説明するように結果を説明してみてください。もし「彼らはProだから」としか答えられない(あるいは「コードがそう言っている」)なら、誰かが月途中でアップグレードしたり一回限りの契約が発生したときに痛みが出ます。

以下のチェックリストを使ってください:

  • データ(subscription items、add-ons、overrides、時間ウィンドウ)だけで「なぜこのユーザーがアクセスを持っているのか?」に答えられるか。
  • すべてのアクセスチェックがプラン名ではなく安定した機能キー(feature.export_csv のような)に基づいているか。プラン名は変わるが機能キーは変えない。
  • エンタイトルメントにトライアル、猶予期間、予定キャンセルを含む明確な開始と終了があるか。時間が欠けているとダウングレードが論争になります。
  • 1顧客に対してオーバーライドレコードでアクセスを付与/削除できるか。これにより「今月だけ席を10追加してくれ」といった要望にコード分岐なしで対応できます。
  • アップグレードとダウングレードをサンプル行でテストして予測可能な結果が得られるか。再現に複雑なスクリプトが必要ならモデルが不十分です。

実務的なテスト:3人のユーザー(新規、月中アップグレード、キャンセル)と1つのアドオン(例:「追加席」や「高度なレポート」)を作り、それぞれに対してアクセスクエリを実行します。結果が明白で説明可能なら準備OKです。

AppMasterのようなツールを使う場合も同じルールを守ってください:1箇所(1つのクエリまたはBusiness Process)に can they? の責任を持たせ、すべてのWeb/Mobile画面が同じ答えを使うようにします。

次のステップ:アップグレードを保守しやすくする

アップグレードを健全に保つ最良の方法は、小さく始めることです。実際に価値を生む少数の機能(5〜10個で十分)を選び、1つのエンタイトルメントクエリ/関数で「今このアカウントはXができるか?」に答えられるようにしてください。一箇所で答えられないなら、アップグレードは常にリスキーに感じられます。

その1つのチェックが動いたら、アップグレード経路を単なる請求動作ではなく製品挙動として扱ってください。変なエッジケースを見つける最速の方法は、実際の顧客の動きをベースにした少数のアクセステストを書くことです。

すぐに効果のある実践的な次の一手:

  • 最小限の機能カタログを定義し、各プランを明確なエンタイトルメントにマップする。
  • アドオンをプランルールに紛れ込ませず、別アイテムとして追加/延長する。
  • よくあるパス(中途アップグレード、更新時のダウングレード、アドオンの追加と削除、トライアルから有料、猶予期間)について5〜10個のアクセステストを書く。
  • 価格変更はデータだけで行う:プラン行、機能マッピング、エンタイトルメント付与を更新し、アプリコードは触らない。
  • 習慣にする:新しいプランやアドオンを追加するたびに少なくとも1つのテストを追加し、アクセスが期待どおりであることを証明する。

ノーコードバックエンドを使う場合でもこのパターンはきれいにモデル化できます。AppMasterのData Designerはコアテーブル(plans、features、subscriptions、subscription items、entitlements)を作るのに適しており、Business Process Editorにアクセス判断のフロー(アクティブなエンタイトルメントを読み込み、時間ウィンドウを適用し、許可/拒否を返す)を置けば、散在するチェックを手で書く必要がなくなります。

投資の回収は次の価格変更のときに現れます。ルールを書き換える代わりにデータを編集します:機能をProからアドオンへ移す、エンタイトルメント期間を変える、旧プランの顧客だけ古い付与を残す、など。アクセスロジックは安定し、アップグレードは制御された更新になり、コードスプリントにはなりません。

スキーマを素早く検証したければ、1つのアップグレードと1つのアドオンをエンドツーエンドでモデリングし、アクセステストを実行してから次へ進んでください。

始めやすい
何かを作成する 素晴らしい

無料プランで AppMaster を試してみてください。
準備が整ったら、適切なサブスクリプションを選択できます。

始める