Cycle 3 で LINE通知 + リアルタイムアラートが動いた。カメラが「見て」、AIが「判断して」、LINEが「知らせる」。技術的にはプロダクトとして成立している。
しかし、お金をいただけなければ事業ではない。プロダクトが「動く」と「売れる」の間には、決済フローという巨大な溝がある。
Sprint Cycle 4 では Stripe 決済統合を完了した。Checkout Session、Customer Portal、Webhook handler を Rust API に組み込み、フリーからエンタープライズまで4つの料金プランを構築。ダッシュボードには料金比較UI、プラン変更フロー、利用状況表示を追加した。
1. アーキテクチャ -- Dashboard から Stripe、そしてDBへ
Stripe決済統合の核心は「ユーザーの課金状態をサーバーサイドで正確に追跡する」ことにある。フロントエンドはあくまで表示と遷移のトリガーで、真の状態管理は Webhook 経由で DB に反映される。
Cycle 4 決済アーキテクチャ
Stripe Checkout Session はホスト型の決済ページを生成する。ユーザーはミセバンAIのドメインを離れ、Stripeのセキュアな決済ページでカード情報を入力する。決済完了後、Stripe が Webhook を叩き、サーバー側で plan_tier を更新する。フロントエンドを一切信用しない設計だ。
2. 料金プラン体系
小規模店舗から大規模チェーンまで、段階的にスケールする4つのプランを設計した。
- カメラ 1台
- データ保持 7日
- 基本AI分析
- LINE通知
- ヒートマップ
- API アクセス
- カメラ 4台
- データ保持 30日
- 高精度AI分析
- LINE通知
- ヒートマップ
- API アクセス
- カメラ 16台
- データ保持 90日
- 最高精度AI分析
- LINE通知
- ヒートマップ
- API アクセス
- カメラ 無制限
- データ保持 無制限
- カスタムAIモデル
- LINE通知
- ヒートマップ + 動線分析
- 専用SLA + サポート
フリープランで導入障壁を下げ、実際に効果を実感してからアップグレードする流れだ。スターターの¥9,800/月は従来の有人巡回コスト(月額5-10万円)の10分の1以下。プロプランのヒートマップとAPIは、データドリブンな経営判断を可能にする。
3. 実装の詳細
Stripe クライアント -- 決済の心臓部
Stripe API との通信を一手に担うモジュール。Checkout Session 作成、Customer Portal URL 生成、サブスクリプション状態取得、Webhook 署名検証までを実装。
StripeClient::new()-- API キーと Webhook Secret を受け取り初期化create_checkout_session()-- Price ID + Store ID から Checkout Session URL を生成create_portal_session()-- 既存顧客の Customer Portal URL を生成get_subscription()--stripe_customer_idからサブスクリプション状態を取得verify_webhook_signature()--Stripe-Signatureヘッダーで HMAC 検証PricingPlan-- 4プランの Price ID・機能制限を定義する enum
pub struct StripeClient {
http: reqwest::Client,
api_key: String,
webhook_secret: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct PricingPlan {
pub tier: &'static str,
pub name: &'static str,
pub price_monthly: i64,
pub max_cameras: Option<i32>,
pub retention_days: i32,
pub features: Vec<&'static str>,
pub stripe_price_id: &'static str,
}
pub const PLANS: &[PricingPlan] = &[
PricingPlan {
tier: "free", name: "フリー", price_monthly: 0,
max_cameras: Some(1), retention_days: 7,
features: vec!["基本AI分析"],
stripe_price_id: "",
},
PricingPlan {
tier: "starter", name: "スターター", price_monthly: 9800,
max_cameras: Some(4), retention_days: 30,
features: vec!["高精度AI分析", "LINE通知"],
stripe_price_id: "price_starter_monthly",
},
PricingPlan {
tier: "pro", name: "プロ", price_monthly: 29800,
max_cameras: Some(16), retention_days: 90,
features: vec!["最高精度AI分析", "LINE通知", "ヒートマップ", "API"],
stripe_price_id: "price_pro_monthly",
},
PricingPlan {
tier: "enterprise", name: "エンタープライズ", price_monthly: 49800,
max_cameras: None, retention_days: -1,
features: vec!["カスタムAIモデル", "専用SLA", "全機能"],
stripe_price_id: "price_enterprise_monthly",
},
];
PricingPlan をコード内に定数として定義することで、料金表示・権限チェック・Checkout 生成のすべてが単一の真実の源 (Single Source of Truth) から導かれる。
Stripe Webhook -- 決済イベントの受信と処理
Stripe からのイベントを受信し、データベースのサブスクリプション状態を更新する。署名検証により、なりすましリクエストを完全に排除。
checkout.session.completed-- 新規サブスクリプション開始。stripe_customer_idとplan_tierを stores に設定customer.subscription.updated-- プラン変更。新しい Price ID から tier を逆引きして更新customer.subscription.deleted-- 解約。plan_tierをfreeにリセットinvoice.payment_failed-- 支払い失敗。アカウントにフラグを立て、オーナーに通知
async fn handle_stripe_webhook(
State(state): State<AppState>,
headers: HeaderMap,
body: String,
) -> Result<StatusCode, ApiError> {
// 1. 署名検証 -- なりすまし防止
let sig = headers.get("Stripe-Signature")
.ok_or(ApiError::Unauthorized)?;
state.stripe.verify_webhook_signature(&body, sig)?;
let event: StripeEvent = serde_json::from_str(&body)?;
match event.event_type.as_str() {
"checkout.session.completed" => {
let session = event.data.object;
let store_id = session.metadata.get("store_id")
.ok_or(ApiError::BadRequest)?;
let tier = price_id_to_tier(&session.price_id);
// DB更新: stripe_customer_id + plan_tier
update_store_billing(
&state.pool, store_id,
&session.customer, tier,
).await?;
}
"customer.subscription.updated" => {
let sub = event.data.object;
let tier = price_id_to_tier(&sub.items[0].price.id);
update_store_tier_by_customer(
&state.pool, &sub.customer, tier,
).await?;
}
"customer.subscription.deleted" => {
let sub = event.data.object;
update_store_tier_by_customer(
&state.pool, &sub.customer, "free",
).await?;
}
"invoice.payment_failed" => {
let invoice = event.data.object;
flag_payment_failed(
&state.pool, &invoice.customer,
).await?;
}
_ => { /* 未処理イベントは無視 */ }
}
Ok(StatusCode::OK)
}
price_id_to_tier() は Stripe の Price ID を内部のプラン名に逆引きする関数だ。Checkout Session の metadata に store_id を埋め込むことで、Webhook 受信時にどの店舗のサブスクリプションかを特定できる。
4. Webhook イベントフロー
Stripe Webhook は4つのイベントを処理する。それぞれが異なるビジネスロジックにマッピングされる。
Webhook イベント → アクション マッピング
plan_tier を更新
機能制限を即時反映
カメラ1台・7日保持に制限
LINE + ダッシュボードで通知
すべてのイベントで署名検証を最初に実行する。Stripe の Webhook Secret で HMAC-SHA256 を検証し、正当なリクエストのみを処理する。不正リクエストは 401 で即座に拒否。
5. ダッシュボード -- 課金UI
ダッシュボード -- 料金・プラン管理UI
ダッシュボードに課金セクションを追加。現在のプラン表示、4プラン比較グリッド、Checkout フロー、Customer Portal リンクを統合。
- 4カラム料金グリッド -- フリー / スターター / プロ / エンタープライズを横並びで比較
- 現在のプラン表示 -- 利用中のプラン名、次回請求日、利用状況 (カメラ数/上限) をリアルタイム表示
- Checkout フロー -- 「アップグレード」ボタン → API → Stripe Checkout Session URL → リダイレクト
- Customer Portal リンク -- カード変更、プラン変更、解約を Stripe ホスト型ページで処理
- 利用量バー -- カメラ数 / データ保持日数をプログレスバーで可視化
// Dashboard: Checkout フロー
async function handleUpgrade(tier) {
const res = await fetch('/api/v1/billing/checkout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ plan_tier: tier }),
});
const { checkout_url } = await res.json();
// Stripe Checkout ページにリダイレクト
window.location.href = checkout_url;
}
// Dashboard: Customer Portal (カード変更・解約)
async function openPortal() {
const res = await fetch('/api/v1/billing/portal', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
});
const { portal_url } = await res.json();
window.location.href = portal_url;
}
フロントエンドは URL を受け取ってリダイレクトするだけ。カード情報は一切ミセバンAIのサーバーを通らない。PCI DSS への準拠負担をゼロにする設計だ。
6. Before / After -- 「無料デモ」から「課金プロダクト」へ
| 項目 | Cycle 3 (Before) | Cycle 4 (After) |
|---|---|---|
| 収益化 | なし。完全無料 | Stripe サブスクリプション (4プラン) |
| 決済フロー | なし | Checkout Session → Webhook → DB更新 |
| プラン管理 | 全ユーザーが同一機能 | tier に応じた機能制限 (カメラ数・保持日数) |
| カード管理 | なし | Stripe Customer Portal (ホスト型) |
| サブスク状態 | 追跡なし | Webhook で DB にリアルタイム反映 |
| DB スキーマ | stores テーブルに plan_tier なし | stripe_customer_id, line_user_id, plan_tier カラム追加 |
| ダッシュボード | 機能表示のみ | 料金比較 + プラン変更 + 利用状況表示 |
最大の変化は「プロダクトからビジネスへの転換」だ。技術的に優れていても収益がなければ持続しない。Stripe統合により、ミセバンAIはユーザーに価値を提供し、対価をいただける事業体になった。
7. 新規APIエンドポイント
| Method | Path | 説明 | 認証 |
|---|---|---|---|
| GET | /api/v1/billing/pricing | 全プランの料金・機能一覧を返却 | Public |
| POST | /api/v1/billing/checkout | Stripe Checkout Session URL を生成 | JWT |
| POST | /api/v1/billing/portal | Stripe Customer Portal URL を生成 | JWT |
| GET | /api/v1/billing/subscription | 現在のサブスクリプション状態を取得 | JWT |
| POST | /api/v1/webhooks/stripe | Stripe Webhook イベント受信 (署名検証) | Stripe Sig |
Cycle 3 までの12エンドポイントと合わせ、合計17エンドポイント。GET /billing/pricing は認証不要で、ランディングページからも呼べる。POST /webhooks/stripe は Stripe 署名ヘッダーで認証を代替する。
8. DB スキーマ変更
stores テーブルへの課金カラム追加
既存の stores テーブルに Stripe 連携用のカラムを追加するマイグレーション。
stripe_customer_id-- Stripe Customer ID (cus_xxx)。Checkout 完了時に設定plan_tier-- 現在のプラン (free/starter/pro/enterprise)。デフォルトfreeline_user_id-- LINE ユーザー ID。Cycle 3 の LINE連携で使用 (今回正式にカラム化)payment_failed-- 支払い失敗フラグ。invoice.payment_failed時にtrue
-- 004_billing.sql
ALTER TABLE stores
ADD COLUMN stripe_customer_id TEXT UNIQUE,
ADD COLUMN plan_tier TEXT NOT NULL DEFAULT 'free'
CHECK (plan_tier IN ('free', 'starter', 'pro', 'enterprise')),
ADD COLUMN line_user_id TEXT,
ADD COLUMN payment_failed BOOLEAN NOT NULL DEFAULT false;
-- インデックス: Webhook で customer_id から店舗を逆引き
CREATE INDEX idx_stores_stripe_customer
ON stores (stripe_customer_id)
WHERE stripe_customer_id IS NOT NULL;
-- RLS: 自店舗の billing 情報のみ読み書き可能
CREATE POLICY "billing_own_store" ON stores
FOR ALL USING (auth.uid() = owner_id);
stripe_customer_id に UNIQUE 制約を付けることで、1店舗 = 1 Stripe Customer の整合性を保証する。plan_tier の CHECK 制約は、不正な値の混入を DB レベルで防ぐ。
9. 技術的判断
なぜこの設計にしたか
- Stripe Checkout (ホスト型) -- カード情報がミセバンAIのサーバーを通らない。PCI DSS 準拠コストゼロ。カスタムフォームは見た目は良いが、セキュリティ負債が大きすぎる
- Webhook ドリブンな状態管理 -- フロントエンドの「決済完了」コールバックを信用しない。Stripe Webhook のみが plan_tier を更新する。リトライ保証あり
- Customer Portal -- カード変更・プラン変更・解約の UI を Stripe に委譲。自前で作ると法的要件 (特定商取引法) への対応が複雑
- PricingPlan を定数で定義 -- DB やコンフィグファイルではなくコード内の定数。プランは頻繁に変わらず、型安全性の方が重要
- HMAC 署名検証 --
Stripe-Signatureヘッダーをhmac-sha256で検証。リプレイ攻撃対策にタイムスタンプも検証 - metadata に store_id を埋め込み -- Checkout Session 作成時に store_id を metadata に入れることで、Webhook 受信時の逆引きを確実にする
10. 成果と数字
作成・更新したファイル
crates/api/src/billing.rs-- StripeClient、PricingPlan 定義、Checkout/Portal/Subscription (280行)crates/api/src/main.rs-- 5エンドポイント追加、StripeClient 初期化 (+95行)crates/api/Cargo.toml-- hmac, sha2 依存追加web/dashboard/billing.js-- 料金グリッド、Checkout フロー、Portal リンク (185行)web/dashboard/index.html-- 課金セクション、利用状況バー追加web/dashboard/billing.css-- 料金カード、プラン比較のスタイル (92行)supabase/migrations/004_billing.sql-- stripe_customer_id, plan_tier, line_user_id カラム (28行)web/landing/index.html-- ランディングページ料金セクションを API 連動に更新
技術がどれだけ優れていても、ユーザーがお金を払う仕組みがなければ事業は成立しない。Cycle 4 で、ミセバンAIは「動くデモ」から「売れるプロダクト」になった。
11. 次のサイクルで何をするか
Cycle 5 は本番運用 (Production Readiness)だ。具体的には以下を進める。
- 監視・ログ基盤 -- 構造化ログ、エラートラッキング、アップタイム監視
- カスタムドメイン --
api.misebanai.com/app.misebanai.comのDNS設定とTLS - CI/CD パイプライン -- GitHub Actions でテスト → ビルド → デプロイの自動化
- パフォーマンスチューニング -- API レスポンスタイム、DB クエリ最適化、キャッシュ戦略
- セキュリティ監査 -- rate limiting、入力バリデーション、CORS 設定の総点検
4つのサイクルで「見て」「判断して」「知らせて」「稼ぐ」仕組みが揃った。Cycle 5 ではこの仕組みを壊れない基盤の上に乗せる。
コードを書くことは始まりに過ぎない。それを24時間365日、止まらずに動かし続けること。それが本番運用だ。
AIが見守り、売上を伸ばす。次世代の店舗経営を始めませんか?
ミセバンAIは、スプリントサイクルで高速に進化中。
フリープランで今すぐ始めて、効果を実感してからアップグレード。