💳

Stripe決済をこれから個人開発で使うために調査した

に公開

本記事のサマリ

これまで請求情報のみで決済処理は行わない法人向けサービスを運用していた私が、個人開発でのStripe決済導入に向けて実際に調査した過程をまとめました!特にサブスクリプション型決済に焦点を当てて、データベース設計の観点とAPI利用方法について、未経験者が理解すべきポイントを整理してます。

なぜ今Stripe決済を調査するのか

個人開発のプロダクトで収益化を考えたとき、決済処理は避けて通れない要素になります。これまで私が構築してきたサービスは、月初に前月分をまとめて請求書を発行する、いわゆる後払いの法人向けモデルでした。(これはこれで請求金額Dashboardを実装するなどしており、Stripe請求にしちゃいたい気持ち...)
しかし個人開発においては、即座に決済が完了し、自動的にサービスの利用権限が付与されるような仕組みが求められますよね。

そこでそろそろStripeを知らねばと考えた次第です。
多くの個人開発者の方が採用していることは知っていましたが、実際にどのような情報をデータベースで管理し、どのような流れで決済処理を行うのかは曖昧な理解でした。今回、本格的に導入を検討するにあたり、体系的に調査することにしました。

Stripeの基本的な決済フローを理解する

まず、Stripe公式ドキュメントを参考に、基本的な決済フローから整理していきます。

https://stripe.com/docs/payments/accept-a-payment

Stripeでの決済は大きく分けて二つのアプローチがあります。一つは「Payment Intent」を使った単発決済、もう一つは「Subscription」を使った継続決済です。個人開発でよく使われるサブスクリプション型サービスを想定すると、後者の理解が重要になります。

単発決済の場合

大きく分けて2つの方法があります。

  • Checkout Sessionを使う方法
  • Stripe Elementsを使う方法

① Checkout Sessionを使う方法は、決済ページをStripeが用意してくれる方法です。
アプリから購入ボタンを押すと、Stripeの決済ページ(Checkout Session)に遷移して、決済の諸々のユーザーフローはそこで完結します。

② Stripe Elementsを使う方法は、決済ページを自分で用意する方法です。
アプリから購入ボタンを押すと、自分で用意した決済ページに遷移して、決済の諸々のユーザーフローはそこで行います。
ただし、入力されたカード情報などはアプリケーション側で管理しないため、フロントエンド→Stripeにカード情報が送信されます。

下記ではStripe Elementsを使う方法をメインに解説します。
単発決済では以下のような流れになります:

単発決済のフロー

これだけだと、データがアプリケーション側とどうやりとりしているかわかりにくいですね。
あとで解説するのでご安心してください!

サブスクリプション決済の場合

サブスクリプションはより複雑でした。まず、Stripeのダッシュボード上で商品(Product)と価格(Price)を設定する必要があります。その上で:

サブスクリプション決済のフロー

Stripe上で扱うモデルが増えるイメージですね。
この仕組みを見ていて気づいたのは、Stripeでは「顧客」という概念が中核にあることです。単発決済でも顧客を作成することは可能ですが、サブスクリプションでは必須になります。

データベース設計で押さえるべき情報

調査を進める中で最も重要だと感じたのは、アプリケーション側のデータベースでどのような情報を管理するかという点です。Stripe側にもデータは保存されますが、アプリケーションとして必要な情報は適切に同期・管理する必要があります。

ここで迷ったのが「Stripeの情報をどこまでローカルDBに保存するべきか」という判断です。調査した結果、以下のような整理になりました。

保存してOKなデータ vs 絶対に保存NGなデータ

保存OKなデータ

保存NGなデータ

この区別が非常に重要です。特にカード情報は、Stripeが提供するStripe Elements(UI)を使うことで、アプリケーション側で一切触れることなく処理できる仕組みになっています。

// stripe elementsの公式ドキュメント
https://stripe.com/docs/payments/accept-a-payment#create-a-payment-element

具体的なテーブル設計

この原則を踏まえて、実際のテーブル構造を考えてみます。

テーブル設計

usersテーブル:ユーザーとStripe顧客の紐付け

-- ユーザーとStripe顧客の紐付け
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    stripe_customer_id VARCHAR(255) UNIQUE,  -- Stripe連携キー
    created_at TIMESTAMP DEFAULT NOW()
);

このstripe_customer_idが最も重要な連携ポイントです。アプリケーションのユーザーとStripeの顧客を結びつける唯一の情報になります。

subscriptionsテーブル:アクセス制御の判定に使用

-- アクセス制御の判定に使用
CREATE TABLE subscriptions (
    id BIGSERIAL PRIMARY KEY,
    stripe_subscription_id VARCHAR(255) UNIQUE NOT NULL,  -- Stripe連携キー
    status VARCHAR(50) NOT NULL,  -- active/past_due/canceled
    current_period_start TIMESTAMP NOT NULL,  -- 課金期間開始
    current_period_end TIMESTAMP NOT NULL,  -- 課金期間終了
    price_id VARCHAR(255) NOT NULL  -- プラン識別
);

statusの管理が特に重要です。Stripeでは「active」「past_due」「canceled」など様々なステータスがあり、これによってサービスへのアクセス権限を制御する必要があります。

https://stripe.com/docs/billing/subscriptions/overview

current_period_endを見ることで、「次の更新日はいつか」「今は有効期間内か」を判断できます。これはユーザーへの表示にも使えますね。

paymentsテーブル:決済履歴・会計・サポート対応

-- 決済履歴・会計・サポート対応
CREATE TABLE payments (
    id BIGSERIAL PRIMARY KEY,
    stripe_payment_intent_id VARCHAR(255),  -- Stripe連携キー
    amount INTEGER NOT NULL,  -- 金額(セント)
    currency VARCHAR(3) NOT NULL,  -- 通貨コード
    status VARCHAR(50) NOT NULL,  -- succeeded/failed
    card_last4 VARCHAR(4)  -- カード下4桁のみ表示用
);

card_last4は「どのカードで決済したか」をユーザーに示すための表示用です。完全なカード番号は保存しません。

webhook_eventsテーブル:冪等性保証・デバッグ

-- 冪等性保証・デバッグ
CREATE TABLE webhook_events (
    id BIGSERIAL PRIMARY KEY,
    stripe_event_id VARCHAR(255) UNIQUE NOT NULL,  -- イベントID(一意)
    event_type VARCHAR(100) NOT NULL,  -- イベント種別
    processed BOOLEAN DEFAULT FALSE,  -- 処理済みフラグ
    event_data JSONB  -- JSONペイロード
);

このテーブルは後述するwebhook処理の冪等性を保証するために使います。同じイベントが複数回送信されても、重複処理を防ぐ仕組みです。

なぜこの設計なのか

この設計のポイントは「必要最小限の情報だけをローカルに持つ」という点です。

決済の詳細情報や顧客の詳細情報は、必要に応じてStripe APIで取得すれば良いという方針です。例えば、管理画面で過去の決済履歴を詳しく見たい場合は、その時点でAPIを叩いて情報を取得する形ですね。

すべての情報をローカルDBに複製すると、Stripeとの同期ズレが発生するリスクが高まります。アクセス制御に必要な情報だけをwebhookで同期し、それ以外はStripeを信頼する方が堅牢だと判断しました。

webhookによる状態同期の重要性

調査の中で最も理解に時間を要したのがwebhookの仕組みです。Stripeでは決済に関する様々なイベントが発生し、それをwebhookでアプリケーションに通知する仕組みがあります。

例えば、Stripeの用意した決済ページで決済が「成功した」「失敗した」などを、アプリケーション側に通知してくれます!

https://stripe.com/docs/webhooks

サブスクリプションで特に重要なイベントは以下です:

  • customer.subscription.created: サブスクリプション作成
  • customer.subscription.updated: サブスクリプション更新(プラン変更など)
  • customer.subscription.deleted: サブスクリプション削除
  • invoice.payment_succeeded: 請求書の支払い成功
  • invoice.payment_failed: 請求書の支払い失敗

これらのイベントを受信して、データベースの状態を適切に更新する必要があります。

// webhookエンドポイントの例(Express.js)
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
    const sig = req.headers['stripe-signature'];
    const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
  
    let event;
    try {
        event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
    } catch (err) {
        console.log(`Webhook signature verification failed.`, err.message);
        return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    switch (event.type) {
        case 'customer.subscription.updated':
            const subscription = event.data.object;
            // データベースのサブスクリプション情報を更新
            await updateSubscriptionStatus(subscription);
            break;
        case 'invoice.payment_failed':
            // 支払い失敗時の処理
            await handlePaymentFailure(event.data.object);
            break;
        default:
            console.log(`Unhandled event type ${event.type}`);
    }

    res.json({received: true});
});

webhookの処理で注意すべき点は、冪等性の確保です。同じイベントが複数回送信される可能性があるため、重複処理を避ける仕組みが必要になります。

Stripeのテスト環境を使った開発フロー

実装に入る前に、Stripeのテスト環境について理解を深めておくことが重要だと感じました。本番環境での試行錯誤はリスクが高いですし、開発初期段階から実際の決済フローを確認できる環境が整っているのは大きな利点です。

テストモードと本番モードの切り替え

StripeのダッシュボードにはテストモードとLiveモードの切り替えスイッチがあります。それぞれのモードで異なるAPIキーが発行され、完全に独立した環境として動作します。

テストモードでは実際のお金が動くことはなく、テスト用のカード番号を使って決済フローの動作確認ができます。APIキーも pk_test_sk_test_で始まるものになっており、誤って本番環境のキーと混同しないような配慮がされています。

開発時は環境変数でAPIキーを管理し、開発環境とステージング環境ではテストキー、本番環境では本番キーを使うという構成が一般的ですね。

// 環境に応じたAPIキーの設定例
const stripe = require('stripe')(
    process.env.NODE_ENV === 'production' 
        ? process.env.STRIPE_LIVE_SECRET_KEY 
        : process.env.STRIPE_TEST_SECRET_KEY
);

テスト用カード番号と各種シナリオ

Stripeは様々なテストシナリオを検証できるように、複数のテストカード番号を用意しています。

https://stripe.com/docs/testing

基本的な成功パターンのテストには 4242 4242 4242 4242を使います。有効期限は未来の日付であれば何でも良く、CVCも任意の3桁の数字で構いません。これは開発初期段階で最も使うカード番号になります。

しかし実際の運用では、決済が成功するケースだけでなく、様々な失敗パターンにも対応する必要があります。Stripeでは以下のようなテストカードが用意されています:

  • 4000 0000 0000 9995: 残高不足(Insufficient funds)
  • 4000 0000 0000 9987: カード番号の確認失敗(Lost card)
  • 4000 0000 0000 9979: 盗難カード(Stolen card)
  • 4000 0025 0000 3155: 3Dセキュア認証が必要なカード

特に3Dセキュア認証のテストは重要です。欧州圏のカードでは3Dセキュアが義務化されているケースもあり、この認証フローが正しく動作することを確認する必要があります。

webhookのローカル開発

webhookの開発で少し困ったのが、ローカル環境でのテスト方法です。Stripeからのwebhook通知は公開URLに送信されるため、通常はローカル環境で直接受信できません。

これを解決するのがStripe CLIです。Stripe CLIをインストールすると、stripe listenコマンドでローカル環境へのトンネルを作成し、webhook通知をローカルホストに転送できます。

# Stripe CLIのインストール後
stripe login
stripe listen --forward-to localhost:3000/webhooks/stripe

このコマンドを実行すると、Stripeからのwebhookイベントがローカルの開発サーバーに転送されるようになります。実際の決済フローをテストしながら、webhook処理のデバッグができるのは非常に便利です。

さらに、特定のイベントを手動でトリガーすることもできます:

# サブスクリプション更新イベントを手動でトリガー
stripe trigger customer.subscription.updated

これにより、実際にサブスクリプションを作成・更新せずとも、webhook処理のテストができます。開発効率が大幅に向上しますね。

今後の実装に向けて

この調査を通じて、Stripe決済の導入における全体像が見えてきました。特にサブスクリプション型の場合、単純な決済処理だけでなく、継続的な状態管理が重要であることを理解できました。

データベース設計についても、Stripeのデータ構造を理解した上で、アプリケーション側で管理すべき情報を適切に選択する必要があることがわかりました。全ての情報を複製するのではなく、アクセス制御に必要な最小限の情報を同期し、詳細な情報が必要な場合はAPIで取得するというアプローチが現実的だと感じています。

次のステップとして、実際にテスト環境でプロトタイプを作成し、webhookの処理も含めた一連のフローを実装してみる予定です。この調査で得た知識を基に、実際の運用でも問題のない決済システムを構築していきたいと思います。

個人開発においては、決済処理の実装が最初のハードルになることが多いですが、Stripeのようなサービスを適切に活用することで、その負担を大幅に軽減できることを実感しました。今回の調査結果が、同じような状況にある開発者の参考になれば幸いです!一緒に個人開発頑張っていきましょう〜!

株式会社StellarCreate | Tech blog📚

Discussion