1ドメイン統合とStripe決済4フェーズ実装
朝一番にGitの差分を開いて、前日作った独立型ダッシュボードのコードを眺めた。昨日20ファイル書いてデプロイまで通したが、サブドメイン分離のセッション分断問題が頭から離れなかった。30分考えて、ディレクトリごと消す決断をした。
今日の作業は「壊した後に組み直す」ことに終始した。購入管理テーブルの設計からStripe Webhookの接続まで、4フェーズを16コミットで積み上げた。
方針転換: 独立型 → /admin 統合
前日の日記に書いた通り、独立型ダッシュボード(eurekapu-dashboard)には3つの問題があった。
- サブドメイン間のCookie共有ができず再ログインが必要
- 購入状態を確認する場所がない
- 課金導線が別サイトに飛ぶ
1ドメイン・1アプリにすればこれらは全て消える。/admin/ ルートとして管理画面をアプリ内に構築し、DBも1つのD1に集約する方針に切り替えた。
Phase 1: DBスキーマと型定義
購入管理用のテーブルを2つ追加した。
-- migrations/0003_create_user_purchase.sql(買い切り)
-- migrations/0004_create_subscription.sql(サブスクリプション)
user_purchaseテーブルのUNIQUE制約については議論があった。UNIQUE(user_id, product_id) を付けるかどうか。返金後の再購入で同じ組み合わせの行が複数できるケースを考慮し、制約は付けずに stripe_event_log で冪等性を保証する方式を採用した。
型定義は shared/types/purchase.ts に ProductId, UserPurchase, UserSubscription, UserAccessInfo を定義。
Phase 2: アクセス制御ミドルウェア
2つのミドルウェアを実装した。
- admin-guard: 管理者メールのホワイトリストで
/admin/配下を保護 - purchase-gate: 購入状態に応じてコンテンツへのアクセスを制御
管理者メールの定義を環境変数で分離し、開発環境と本番環境で異なるリストを使えるようにした。Codex(GPT-5.3)のレビューで「開発環境のテストアカウントが本番に混入するリスク」を指摘されたのがきっかけ。
メール/パスワードログインフォームの表示制御も環境に応じて切り替えるようにした。本番ではGoogleログインのみ、開発環境ではClaude Codeがテスト用にメール/パスワードでもログインできる。
Phase 3: 管理画面の構築
/admin/ 配下に以下のページを作成した。
- ダッシュボード (
/admin/): KPI指標カード4枚 + 最近のユーザーテーブル - ユーザー一覧 (
/admin/users): 全ユーザーの学習状況一覧 - 回答詳細: ユーザーごとの回答履歴と正答率
- 購入管理 (
/admin/purchases): 決済履歴の確認 - 開発ドキュメント (
/admin/docs): 旧計画と現行マネタイズ計画の比較ページ
レイアウトは左サイドバー240px + メインエリアの構成。管理者でログインしたときだけヘッダーにリンクが出る。
本番デプロイ後、自分のアカウントでログインしたら管理画面に入れなかった。ADMIN_EMAILSの環境変数に自分のメールアドレスが登録されていなかった。エラーメッセージが「Unauthorized」だけだったので、認証の問題かガードの問題かの切り分けに少し手間取った。
Phase 4: Stripe Checkout + Webhook
Stripe Checkoutのセッション作成とWebhook受信の基盤を実装した。
checkout.sessions.completedイベントでuser_purchaseに記録customer.subscription.created/updated/deletedでsubscriptionテーブルを更新- Webhookの署名検証を実装
Stripeアカウントの審査は別メールアドレスで作成済みだったので、環境変数の整理も行った。
D1スキーマの整合性問題と解決
ローカルDBと本番(リモート)D1のスキーマに差分が生じていることに気づいた。原因を追うと、ローカルではBetter Authのマイグレーション生成で作られたスキーマを使っていたが、本番では手動でALTER TABLEを実行していた箇所があった。
対処として以下を実施した。
- ローカルとリモートのスキーマスナップショットをMarkdownで記録 (
memo/2026-02-22/d1-schema-snapshot.md) - 不足していたインデックスを追加するマイグレーション (
0004_add_auth_indexes.sql) - 外部キーのCASCADE設定を追加するマイグレーション (
0005_add_cascade_to_auth_fk.sql) - 本番D1にマイグレーションを適用
マイグレーション運用ガイドとSVGの概要図も作成した。D1のスキーマ管理は「マイグレーションファイルが唯一の真実」という原則を徹底しないと、手動変更が積もって差分が見えなくなる。
Codexレビュー → 指摘反映
実装計画の段階でCodex(GPT-5.3)にプランレビューを依頼し、実装後に未コミット変更のコードレビューも実行した。
プランレビューの主な指摘:
- 管理者メールの環境分離が必要(反映済み)
- 開発用ログイン機能が本番に漏れるリスク(反映済み)
コードレビューの主な指摘:
- メール/パスワードログインが開発環境限定であることのテスト追加(反映済み)
- テストカバレッジの向上(対応済み)
テスト: 全55テスト通過
テストカバレッジを改善するため、以下のテストを追加した。
adminMiddleware.test.ts: admin-guardの権限チェックauthEmail.test.ts: メール/パスワード認証のバリデーションauthPlugin.test.ts: 認証プラグインの初期化errorPage.test.ts: エラーページの表示adminConfig.test.ts: 環境別の管理者設定
ESLint、ビルド、Vitest、Playwright E2Eの全てが通過。E2Eテストのプロジェクト指定オプションも修正した。
UI改善: タブUI導入とコンテンツ並び順
問題一覧ページに「仕分け問題」「回答履歴・復習」のタブ切り替えを導入した。タブ切り替え時にコンテンツ幅が変わってガタつく問題があり、仕分け問題側の幅に固定して解消。全範囲カードを先頭に配置し、ランダム10問もその中に組み込んだ。
画像パスの問題も修正した。一部のコンテンツで外部URL参照になっていた画像をローカルに保存し、Cloudflareから配信するように変更。キャッシュが残っていて「直ったのに表示されない」という現象に遭遇し、シークレットモードで確認して原因を切り分けた。
動画ホスティングリサーチ
コンテンツに動画を含める場合のホスティング先を調査し、ドキュメントとSVG図解にまとめた。
| サービス | 特徴 |
|---|---|
| Bunny.net | コスト効率が高い、HLS配信対応 |
| Cloudflare R2 | 既存インフラとの親和性、エグレス無料 |
| Cloudflare Stream | マネージドだが従量課金が読みにくい |
方針として HLS + AES-128暗号化 を採用することに決めた。鍵配信サーバーをCloudflare Workersで立て、認証状態に応じて鍵を返す仕組みを想定。動画ファイル自体はR2に置く案が有力だが、Bunny.netのCDN品質も捨てがたい。実装は動画コンテンツの需要が見えてからにする。
コンセプトディレクトリの整理も行い、ビジネスモデル図・動画ホスティングコスト比較・ヒートマップのSVGを追加した。
今日の学び
- スキーマスナップショットをMarkdownで管理する運用を始めた。
PRAGMA table_infoの結果をコピペするだけだが、ローカルとリモートの差分が一目で見える。マイグレーションファイルだけでは「今の状態」が分からないことがある - Codexのプランレビューは、実装に入る前の段階で「見落とし」を拾うのに向いている。コードレビューはファイル横断の整合性チェックが得意
- 環境別の設定分離(管理者メール、ログインフォーム表示)は最初から入れておくべきだった。後から追加すると既存テストとの整合性を取る手間が増える
- キャッシュ問題のデバッグは、まずシークレットモードで確認する手順を体に染み込ませる。コード側に問題があると思い込んで無駄な調査をしてしまった
次のステップ
- Stripe審査通過後に本番環境で決済フローをE2Eテスト
- 動画コンテンツの需要が見えたらHLS+AES-128の実装に着手
- Codex P2/P3指摘への対応
- 残りの章の問題データ移行と動作確認