SaaSで「プランごとに利用できる機能を制御する」機能を、Stripeだけで実装する方法


SaaSプロダクトにおける機能や権限の管理は複雑で手間がかかりがちです。専用のDBテーブルや管理ロジック、そしてサーバー側とフロントエンドの実装が必要になる上、価格改定や新プランの提供開始などで定期的に動きを変更する必要が生まれます。この記事では、2024年にStripeが発表した**Entitlements API**を活用して、これらの**プラン別アクセス権限管理機能**を効率的に実装する方法を紹介します。

## SaaSプロダクトにおける機能アクセス権限管理の課題
SaaSプロダクトでは、顧客ごとに異なる料金プランやサブスクリプションに応じて、利用可能な機能を切り替える必要があります。さらに、新サービスの追加や価格の改定などが行われる度に、機能へのアクセス権限の管理ロジックを変更しなければなりません。

これらの管理を自社で行うには、以下のような機能やシステムが必要となります。

- 権限管理システム
- プランや製品と権限のマッピング
- ユーザー情報とプラン・権限のデータ連携
- 価格改定や新プランの追加に伴うシステム改修

これらの開発や運用保守には、少なくないリソースが必要です。そのため、プロダクトのコア機能から注力が逸れがちになり、システムが肥大化し、保守性が低下するなどの問題を生じさせます。

## Entitlements APIで実現する機能アクセス権限管理の外部化

StripeのEntitlements APIを利用することで、機能へのアクセス権限管理を製品の外部に委託することができます。

1. 料金・商品情報と一緒に「アクセス権限（ エンタイトルメント ）」情報もStripeで定義・保存します
1. 商品とアクセス権限を関連付けることで、プランごとに利用できる権限のマッピングを行います
1. 関連付け済みの商品を利用してサブスクリプションを作成するだけで、その顧客がアクセスできる権限を更新できます
1. Stripe上の顧客ID(`cus_xxx`)を利用して、その顧客が持つアクセス権限の一覧を取得できます
1. 利用可能な機能が変更された場合は、webhookでリアルタイムに通知されます

つまり、サービス側では利用可能な機能の一覧を参照するだけで良くなります。

## Entitlements APIで、アクセス権限を顧客に付与する方法

それでは実際にアクセス権限の付与を行ってみましょう。ここからはStripe WorkbenchとStripe CLIを利用して設定を行います。

:::note
**Stripe Workbenchについて**
Stripe Workbenchは、2024/05時点でベータ版提供の機能です。そのため、以下のURLからベータ版機能を有効化する必要があります。

https://dashboard.stripe.com/workbench
:::

### Step1: 提供するプランと機能（アクセス権限）を設計する

まずは「どんな製品をいくらで提供するか」や「どのようなアクセス権を付与すべきか」の設計を行いましょう。例えばウェブサイトやアプリケーションのホスティングサービスでは、多くの場合有料プランでのみ独自ドメインの設定ができます。また、LLMをベースとしたAIチャットアプリケーションでは、上位プランでのみ利用できるモデルが用意されています。今回は有料のオンラインメディアサイトを例にして、以下のようなモデルを用意しました。

|商品名|価格|権限|
|:--|:--|:--|
|Free| 0円|無料記事閲覧|
|Personal| 980円/月|無料記事閲覧 / 有料記事閲覧|
|Business| 4,980円/月|無料記事閲覧 / 有料記事閲覧 / ビジネスレポート閲覧|

今回の例は、１ユーザーが１つのプランを契約する体系を想定したモデルです。そのため、上位プランの商品にも下位プランで提供する機能へのアクセス権限を登録するように設計しました。もしホスティングサービスのCDNオプションのような、複数のプランを組み合わせて契約するモデルでは、次のような設計も考えることができます。


|商品名|価格|権限|
|:--|:--|:--|
|Hobby|980円/月|Webサーバー１台|
|Business|1980円/月|Webサーバー１台 / DBサーバー1台|
|CDN Option| 980~円/月 (従量課金)|CDN 1ディストリビューション|
|Backup Option| 980円/月|10バックアップ|

モデルの設計ができましたので、実装に落とし込みましょう。

### Step2: Stripe上に商品・料金とエンタイトルメントを登録する

先ほど作成したモデルに応じて、商品や料金などの情報をStripeに登録します。まずはページ下にある黒いバーをクリックしましょう。クリックすると、Stripeの新しい開発支援ツール「Workbench」が立ち上がります。

![スクリーンショット 2024-05-21 14.34.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/bc9dfb77-1651-1434-7593-a70c2bc1ac77.png)

Workbenchでは、APIキーやAPIリクエストログ・エラーログそしてWebhookのイベントログなど、Stripeの組み込みや運用保守に必要な情報をまとめて見ることができます。また、StripeのAPIをブラウザ上でテストする**Stripe Shell**機能も用意されており、この記事ではこの**Stripe Shell**を利用して作業を行います。Stripe Shellを起動するため、Workbench上部にあるタブの、**Shell**をクリックしましょう。

![スクリーンショット 2024-05-21 14.34.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/fabdccc1-7710-81bc-4e9e-df46c0e203f6.png)

Stripe Shellが起動しました。ここではStripe CLIをブラウザ上で実行できるだけでなく、StripeのAPIやAPIがサポートするパラメータを確認しながら動作テストまで行うことができる**API Explorer**機能も利用できます。ページ右側にある**API Explorer**の、**Resource**をクリックしましょう。

![スクリーンショット 2024-05-21 14.34.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/924934d0-28fc-910d-9c52-38eed8433c93.png)

利用できるAPIリソースを表示・検索できるUIが表示されます。ここで`product`を検索フォームに入力し、**Products**を選択してください。Stripe上で商品情報を管理するための[Products API](https://docs.stripe.com/api/products)が選択された状態となります。

![スクリーンショット 2024-05-21 14.38.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/06405eaa-3754-5ac0-f8bd-d23b7569eb3d.png)

[Products API](https://docs.stripe.com/api/products)で利用するメソッドを指定するため、**Resource**右側にある**Method**を選択しましょう。今回は新しく商品を登録しますので、`create`と検索フォームに入力して、**Create**メソッドを選択します。これでStripeに商品登録を行うAPIをテストする準備ができました。

![スクリーンショット 2024-05-21 14.39.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/2303a012-bdba-34ac-ac2e-3458a1b5a88a.png)

API Explorerの**Parameters**にこのAPIで利用できるパラメーターが表示されています。ここに商品や料金の情報を入力することで、商品と料金をまとめて登録することができます。

![スクリーンショット 2024-05-21 14.42.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/51279f99-5583-85ab-ecac-171d515df4ad.png)

今回は以下の内容でフォームを入力しましょう。

|name|value|
|:--|:--|
|name|`Free`|
|default_price_data[currency]|`jpy`|
|default_price_data[unit_amount]|`0`|
|default_price_data[recurring][interval]|`month`|

入力が終わると、次のような表示になります。

![スクリーンショット 2024-05-21 14.46.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/f805b64e-07ce-17e8-1bc2-ba47e10208dc.png)

また、ページ下部にはAPI Explorerで設定した値がStripe CLIのパラメータとして表示されています。表示されているStripe CLIコマンドをコピーすることで、手元の環境で実行させたり、Wikiや社内チャットにコマンドを共有することもできます。

![スクリーンショット 2024-05-21 14.46.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/7efecf43-4693-7e53-56b0-1200fd9be974.png)

もしAPI Explorerの操作がうまくいかない場合は、下のコマンドをコピーして**Shell**に貼り付けてください。

```bash
stripe products create --name="Free" \
 -d "default_price_data[currency]=jpy" \
 -d "default_price_data[unit_amount]=0" \
 -d "default_price_data[recurring][interval]=month" 
```

コマンドを入力した状態で**[Enterキー]**を押すか、ページ右下の**Run**ボタンをクリックすることでコマンドが実行されます。

![スクリーンショット 2024-05-21 14.48.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/b12156da-32f1-abf2-726a-8439d3ac6a09.png)

:::note
**コマンドを言語別のSDKに変換する**
API ExplorerでテストしたAPIリクエストを実装に反映させるのはとても簡単です。API Explorer下部にある**Print SDK request**リンクをクリックすると、その右側に表示されている言語で同じ内容のAPIリクエストを送信するコードが出力されます。

![スクリーンショット 2024-05-21 14.51.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/fcd047d3-8396-3f27-54df-5c6b713eda29.png)

:::

残りの2商品についても、Stripe Shellで登録しましょう。以下にコマンドを用意しましたので、コピーアンドペーストで実行してください。

```bash
stripe products create --name="Business" \
 -d "default_price_data[currency]=jpy" \
 -d "default_price_data[unit_amount]=4980" \
 -d "default_price_data[recurring][interval]=month" 
```

```bash
stripe products create --name="Personal" \
 -d "default_price_data[currency]=jpy" \
 -d "default_price_data[unit_amount]=980" \
 -d "default_price_data[recurring][interval]=month" 
```

ダッシュボードの[商品カタログ](https://dashboard.stripe.com/test/products?active=true)ページに、3商品が表示されていれば、登録成功です。

![スクリーンショット 2024-05-21 14.53.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/f546f6df-c6f2-9832-2210-1bcae29841f5.png)



### Step3: 商品へアクセス権限（エンタイトルメント）を登録する

商品と料金の設定ができましたので、続いて商品ごとの権限設定を行いましょう。Stripeでは、顧客に付与するアクセス権限はプロダクト単位で管理します。それぞれの権限は、`Feature`リソースとして登録する必要があり、それを`Product`に紐づける形です。

![スクリーンショット 2024-05-21 14.57.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/84a2b40c-4f03-5bc0-728e-76974b779a74.png)

#### 機能を`Entitlements Features`として登録する

まずはFeatureリソースを作成しましょう。API Explorerで以下の値を入力して、3つの権限を登録しましょう。Resourceは`Entitlments Features`、　Methodは`Create`を利用します。

![スクリーンショット 2024-05-21 15.06.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/a3d3d974-0f63-c7ff-4ff9-de52062ceb73.png)
||無料記事閲覧 | 有料記事閲覧 | ビジネスレポート閲覧 |
|:--|:--|:--|:--|
|**name**|無料記事閲覧 | 有料記事閲覧 | ビジネスレポート閲覧 |
|**lookup_key**| free_posts | paid_posts | biz_reports |


またはStripe Shellに以下の３コマンドをコピーアンドペーストしてもOKです。

```bash
stripe entitlements features create --lookup-key="free_posts" --name="無料記事閲覧" 
stripe entitlements features create --lookup-key="paid_posts" --name="有料記事閲覧"
stripe entitlements features create --lookup-key="biz_reports" --name="ビジネスレポート閲覧"
```

エラーメッセージが表示されなければ、登録成功です。

![スクリーンショット 2024-05-21 15.07.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/a36c0c8b-151b-c651-3995-1f2f2178f0bb.png)

続いて登録した機能を各商品に紐づけましょう。

#### 登録した機能を、商品に紐づける

商品を通してユーザーにアクセス権限を付与するには、商品と機能の関連付けが必要です。これもStripeではAPIを利用して実施します。紐付けには、商品IDが必要です。ダッシュボードの商品カタログページで、付与したい商品の詳細ページに移動しましょう。

![スクリーンショット 2024-05-21 14.53.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/f546f6df-c6f2-9832-2210-1bcae29841f5.png)

Workbenchを開いた状態で、商品の詳細ページに移動すると、画面右下に`prod_`から始まる文字列が表示されます。
![スクリーンショット 2024-05-21 15.11.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/52a2dbf0-73b9-1d70-0537-6432b101ca7a.png)

これをクリックすると、今アクセスしているページのリソース内容をJSONで確認できる「Inspect」タブに移動します。これを利用して、GUIで作成したリソースなどのパラメータを確認することができ、デバッグやAPIリクエストへの落とし込みなどがスムーズに行うことが可能となります。

![スクリーンショット 2024-05-21 15.11.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d0a3448b-ec46-2849-e0d4-2c1df30ff891.png)

[Inspect]で表示されている商品IDをコピーして、[Shell]タブに戻りましょう。API　Explorerを開いて、Resourceは`Product Features`、　Methodは`Create`を選択しましょう。**Path arguments**の`id`に、コピーした商品IDを保存します。

![スクリーンショット 2024-05-21 15.17.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/ed7f0de7-b6fc-cc8c-d956-4fa4902de973.png)

続いて紐づける機能IDについては、`stripe entitlements features list`コマンドで一覧表示できます。一覧で表示されますので、`id`にある`feat_`から始まるIDをコピーしておきましょう。

![スクリーンショット 2024-05-21 15.17.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/072a87f9-f197-ee7b-0c91-5b00e9ff53c8.png)

コピーしたIDを、API Explorerで表示中のパラメータ`entitlment_feature`に設定しましょう。これで商品と機能を紐付けするリクエストの準備ができました。

![スクリーンショット 2024-05-21 15.18.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/e055629c-2a44-d737-19ae-dc04bb5eb83f.png)

あとは生成されたStripe CLIコマンドを商品へ登録する機能ごとに実行するだけです。

```bash
stripe product_features create "Freeの商品ID" --entitlement-feature="無料記事閲覧の機能ID"

stripe product_features create "Personalの商品ID" --entitlement-feature="無料記事閲覧の機能ID"
stripe product_features create "Personalの商品ID" --entitlement-feature="有料記事閲覧の機能ID"

stripe product_features create "Businessの商品ID" --entitlement-feature="無料記事閲覧の機能ID"
stripe product_features create "Businessの商品ID" --entitlement-feature="有料記事閲覧の機能ID"
stripe product_features create "Businessの商品ID" --entitlement-feature="ビジネスレポート閲覧の機能ID"
```

全てのコマンドを実行すると、紐付け完了です。`stripe product_features list prod_から始まる商品ID`を実行することで、商品ごとに設定されている機能を見ることができます。


![スクリーンショット 2024-05-21 15.23.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/89cacb74-2be3-4142-c683-e670892d6f87.png)

ここまでで商品ごとのアクセス権限設定と、顧客がサブスクリプションを開始するための前準備が完了しました。

### Step4: 顧客を登録し、サブスクリプションを作成する

商品側の設定が完了したので、顧客とサブスクリプションデータを作成してみましょう。ページ上部にあるサブスクリプション作成ボタンをクリックして、作成フォームを表示されましょう。

![スクリーンショット 2024-05-21 15.24.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/c98bc8ce-768f-f8ca-bfef-a2b90f01fac9.png)

フォームにテスト用のメールアドレスを入力し、先ほど作成した商品のどれか１つをサブスクリプションの詳細に設定します。その後、支払いに利用するクレジットカードを、`4242424242424242`などのテスト用カード番号で登録して、作成を完了させましょう。

![スクリーンショット 2024-05-21 15.26.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/8b47da68-1e16-346f-a974-a7acf91e0437.png)

サブスクリプションが新しくダッシュボードに表示されていれば、作成成功です。ここの実装方法は、Entitlement APIを利用していない場合と同じです。そのため、Stripe CheckoutやStripe Elementsを利用した場合の実装も、このステップでは追加の作業はありません。

### Step5: 顧客にアクセス権限が付与されているかを確認する

サブスクリプションが作成できましたので、顧客にアクセス権限が付与されているかを確認してみましょう。アクセス権限の確認には、顧客のIDが必要です。サブスクリプション一覧ページから、[顧客を表示]ボタンをクリックすることで、顧客情報詳細ページに移動できます。

![スクリーンショット 2024-05-21 15.27.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/31aa76a8-9b33-49af-58ee-d25a80e39977.png)

詳細ページのURLやUIなどで、`cus_`から始まる顧客IDをコピーしましょう。あとはStripe Shellでこの値を利用したコマンドを実行するだけです。

![スクリーンショット 2024-05-21 15.29.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/f7f52990-35b2-177e-c05f-9818dd006563.png)

```bash
stripe active_entitlements list \
--customer="cus_から始まる顧客ID" 
```

このコマンドを実行すると、サブスクリプションへ設定した商品に対応したアクセス権限が取得できます。

![スクリーンショット 2024-05-21 15.33.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d3544f83-a8a6-14f6-815b-09708b2a21f8.png)

このAPIレスポンスで、アクセス権限の名前まで取得したい場合は、`expand`パラメータを追加しましょう。

```diff
stripe active_entitlements list \
+ -d "expand[0]=data.feature" \
 --customer="cus_から始まる顧客ID"
```

![スクリーンショット 2024-05-21 15.35.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/f9cf3822-ade8-b826-9dde-039933a61daf.png)

アプリケーションの組み込みでは、`lookup_key`に設定したキーを利用して表示や処理の制御を行います。 下の[Hono](https://hono.dev)を利用したAPIでは、`biz_reports`のキーを持つエンタイトルメントを付与されていない顧客は、プランアップグレードを促すメッセージが表示されます。

```js
app.get('/cus', async c => {
    const stripe = new Stripe('sk_test_xxx')
    const requiredKey = 'biz_reports'
    const entitlements = await stripe.entitlements.activeEntitlements.list({
        customer: 'cus_xxxxA'
    })
    const paidPostFeature = entitlements.data.find(ent => {
        return ent.lookup_key === requiredKey
    })
    if (!paidPostFeature) {
        const { data: features } = await stripe.entitlements.features.list({
            lookup_key: requiredKey
        })
        c.status(429)
        return c.json({
            message: `このページにアクセスするには、「${features[0].name}」権限が必要です。プランをアップグレードしてください。`
        })
    }
    return c.text("Welcome to paid post page!")
})
```


### Step6: サブスクリプションを解約して、アクセス権限を喪失することを確認する

最後に付与したアクセス権限を、解約やダウングレードで喪失できるかを確認しましょう。ダッシュボードでサブスクリプションを解約操作してください。

![スクリーンショット 2024-05-21 15.43.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d0e33f34-f81e-22c0-3f2a-304a7da4cc36.png)

解約処理が成功したのを確認してから、再びその顧客に付与されたアクセス権限の一覧を取得しましょう。

```bash
stripe active_entitlements list \
--customer="cus_から始まる顧客ID" 
```

他にサブスクリプションを契約していない場合、レスポンスが空の配列に変わります。

![スクリーンショット 2024-05-21 15.45.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/7f130617-b4e4-6e04-6db2-1b60a00d8ab8.png)

これによって、プラン変更時に起こるアクセス権限の変化にも、Stripe内のデータを参照するだけで対応できるようになります。

## [Advanced Tips] バックエンドで権限変更のワークフローを動かす方法

インフラの構成変更やシステムの設定変更など、サービスによっては権限の喪失や付与に連動したワークフローを実行することがあります。このような権限の変化についても、StripeのWebhookイベントを利用することで、シームレスに連携できます。

プランの変更や解約などに伴う権限の変化に対応するには、新しく追加された`entitlements.active_entitlement_summary.updated`イベントを利用します。このイベントには、「対象の顧客ID」や「変更前と現在の権限設定」などの情報が含まれています。

```json
{
  "object": {
    "object": "entitlements.active_entitlement_summary",
    "customer": "cus_OYXmEvKfYDp7DA",
    "entitlements": {
      "object": "list",
      "data": [],
      "has_more": false,
      "url": "/v1/customer/cus_OYXmEvKfYDp7DA/entitlements"
    },
    "livemode": false
  },
  "previous_attributes": {
    "entitlements": {
      "data": [
        {
          "id": "ent_test_61QSA7cL7mjHYxePl41IDF6qBhttt65Y",
          "object": "entitlements.active_entitlement",
          "feature": "feat_test_61QS9o4tAnK9ATPJX41IDF6qBhttt7Cq",
          "livemode": false,
          "lookup_key": "paid_posts"
        },
        {
          "id": "ent_test_61QSA7cYW9h6XG8zt41IDF6qBhttt27s",
          "object": "entitlements.active_entitlement",
          "feature": "feat_test_61QS9nd2kxnCe1ksR41IDF6qBhttt6Ui",
          "livemode": false,
          "lookup_key": "free_posts"
        }
      ]
    }
  }
}
```

このデータを利用することで、「`previous_attributes`にしかない権限がある場合、それに関連したリソースやDBを変更するワークフローを実行する」といった設定ができます。



## まとめ

新しく発表された**Entitlement API**を利用することで、プランごとにユーザーへ付与するアクセス権限の管理についてもStripeで管理できるようになります。すでに権限管理をIDPサービスや独自のDBで管理している場合でも、Webhookイベントを利用することで権限の付与や喪失をリアルタイムで反映させることが可能です。

新しいサービスのローンチとPMFをより高速に実現させるため、ぜひサブスクリプションにStripeをご利用ください。

## Document:

https://docs.stripe.com/billing/entitlements

https://docs.stripe.com/api/entitlements/feature/object

https://docs.stripe.com/api/product-feature

https://docs.stripe.com/api/entitlements/active-entitlement