• #eurekapu
  • #nuxt4
  • #stripe
  • #決済
  • #管理画面
  • #d1
  • #アクセス制御
  • #codex
  • #動画ホスティング
  • #日記
開発eurekapu-nuxt4メモ

1ドメイン統合とStripe決済4フェーズ実装

朝一番にGitの差分を開いて、前日作った独立型ダッシュボードのコードを眺めた。昨日20ファイル書いてデプロイまで通したが、サブドメイン分離のセッション分断問題が頭から離れなかった。30分考えて、ディレクトリごと消す決断をした。

今日の作業は「壊した後に組み直す」ことに終始した。購入管理テーブルの設計からStripe Webhookの接続まで、4フェーズを16コミットで積み上げた。

方針転換: 独立型 → /admin 統合

前日の日記に書いた通り、独立型ダッシュボード(eurekapu-dashboard)には3つの問題があった。

  1. サブドメイン間のCookie共有ができず再ログインが必要
  2. 購入状態を確認する場所がない
  3. 課金導線が別サイトに飛ぶ

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.tsProductId, 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 / deletedsubscription テーブルを更新
  • Webhookの署名検証を実装

Stripeアカウントの審査は別メールアドレスで作成済みだったので、環境変数の整理も行った。

D1スキーマの整合性問題と解決

ローカルDBと本番(リモート)D1のスキーマに差分が生じていることに気づいた。原因を追うと、ローカルではBetter Authのマイグレーション生成で作られたスキーマを使っていたが、本番では手動でALTER TABLEを実行していた箇所があった。

対処として以下を実施した。

  1. ローカルとリモートのスキーマスナップショットをMarkdownで記録 (memo/2026-02-22/d1-schema-snapshot.md)
  2. 不足していたインデックスを追加するマイグレーション (0004_add_auth_indexes.sql)
  3. 外部キーのCASCADE設定を追加するマイグレーション (0005_add_cascade_to_auth_fk.sql)
  4. 本番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指摘への対応
  • 残りの章の問題データ移行と動作確認