Better Auth認証基盤の本番投入と問題集機能6フェーズ拡張
前日にBetter Auth + Google OAuthの認証スケルトンと管理ダッシュボードを作り、ビルドが通るところまでは確認していた。今日は認証を本番環境に載せるところから始まり、問題集の機能拡張を6フェーズで一気に積み上げた。朝から夜までコードを書き続けて、気づいたら21ファイル・1029行が増えていた。
認証基盤の本番投入
.env設定とdevサーバーのポート固定
Better Authが参照する環境変数(BETTER_AUTH_SECRET、Google OAuthのクライアントID/シークレット)を.envに整理した。開発時のコールバックURLを安定させるため、devサーバーのポートを3200に固定。他プロジェクトの3000番とぶつかる問題がこれで消えた。
D1マイグレーション
Better Authが必要とする4テーブル(user、session、account、verification)のマイグレーションSQLを作成し、wrangler d1 executeで本番D1に適用した。前日のダッシュボード実装で踏んだ「tokenカラム不足」問題の教訓を活かし、適用後にPRAGMA table_infoで全テーブルの定義を目視確認した。
GCP ConsoleでのOAuth設定
Google Cloud ConsoleでOAuthクライアントの承認済みリダイレクトURIに本番ドメインを追加した。承認済みJavaScriptオリジンも忘れずに設定。ここを漏らすとリダイレクト先が不一致でOAuthフローが途中で止まるので、URIの一覧をスクショに残しておいた。
メール/パスワード認証は見送り
当初はメール/パスワード認証も入れる予定だったが、見送った。メール配信(パスワードリセット等)にはSESやResendの契約が必要で、月額コストが発生する。個人開発で固定費を増やしたくない。Googleログイン一本に絞れば、認証インフラの維持コストはゼロになる。迷ったが、ユーザー数が少ないうちは選択肢を減らす方が正しいと判断した。
Stripe審査待ちで決済は保留
買い切り決済にStripe Checkoutを使う予定だったが、アカウント審査が完了していない。認証基盤が先に動く状態になったので、決済部分は審査通過後に実装する。今日のスコープからは外した。
Progressive Engagementの設計
Duolingo型のProgressive Engagement方式でユーザー登録導線を設計した。いきなり「ログインしてください」と壁を出すのではなく、使い込むほど登録のメリットが見えてくる段階的なアプローチ。
アクセス3段階:
- 未ログイン(Free) -- 問題を解ける。学習履歴はローカルストレージに保存
- ログイン済み -- 学習履歴がD1に永続化される。復習機能が使える
- 購入済み -- 全コンテンツにアクセス可能
この設計が問題集機能の拡張方針を決定づけた。
問題集機能6フェーズ拡張
認証基盤が動いたところで、問題集の機能拡張に取り掛かった。6フェーズに分けて実装を進めた。
Phase A: 基盤整備
各問題にユニークなQuestion IDを生成し、JSONデータに埋め込んだ。D1側にも学習履歴テーブルのマイグレーションを追加。問題IDがないと履歴をどの問題に紐づけるかが定まらないので、ここが全ての土台になる。
Phase B: 履歴インフラ
useQuizHistory composableを作成。ログイン状態に応じて保存先を切り替える。
- 未ログイン --
localStorageに保存 - ログイン済み -- サーバーAPIを経由してD1に保存
サーバー側には回答記録のPOSTと履歴取得のGETを実装。composableが保存先の違いを吸収するので、UIコンポーネントは保存先を意識しない。
Phase C: 履歴表示
QuizHistoryDotsコンポーネントを実装した。各問題の正誤を小さなドット(緑=正解、赤=不正解、灰=未回答)で横一列に並べる。一目でどこを間違えたかが視覚的にわかる。
復習ページreview.vueも新規作成。間違えた問題だけをフィルタリングして再挑戦できる。
Phase D: 回答記録の統合
practice.vue(問題演習画面)に回答記録ロジックを統合した。問題を解くたびにuseQuizHistoryを通じて記録が走る。
未ログインユーザーに対しては、一定問題数を解いた後に「ログインすると学習履歴が保存されます」というモーダルを表示する。強制ではなく、閉じればそのまま続けられる。
Phase E: アクセスレベル制御
accessConfigで問題集ごとのアクセスレベル(Free / Login / Purchased)を定義し、useQuizAccess composableで現在のユーザー状態と照合する。
アクセス権がないコンテンツにはロックアイコンを表示。問題一覧ページで「この先はログインが必要」「この先は購入が必要」が視覚的にわかるようにした。
Phase F: ランダム10問出題
random.vueを新規作成。全問題プールからランダムに10問を選んで出題する。「今日のチャレンジ」的な使い方を想定している。シャッフルアルゴリズムはFisher-Yatesを使い、偏りが出ないようにした。
復習ページの改善
「要復習」フィルターの3段階分割
最初は「要復習」を1つのフィルターにしていたが、不正解の回数によって緊急度が違う。不正解の回数で3段階に分割した。
- 1回間違い -- 軽い復習
- 2回間違い -- 重点復習
- 3回以上 -- 要注意
各フィルターに該当する問題の件数も表示し、「あと何問残っているか」が数字で見えるようにした。
3階層表示
復習ページの構造を「章 → セクション → 問題」の3階層に整理した。フラットに問題が並んでいると、全体のどの部分を復習しているのかが掴めない。章見出しとセクション見出しを挟むことで、教科書的な構造が復習ページにも反映されるようになった。
Codex(GPT-5.3)コードレビュー
6フェーズの実装が完了した時点で、Codex CLIにコードレビューを依頼した。指摘はP1(即修正)、P2(近日中に修正)、P3(改善推奨)に分類されて返ってきた。
P1の修正を即座に対応し、P2とP3は次回以降のタスクとして記録した。Codexはファイル横断のレビューが得意で、composable間の型不整合やAPIエンドポイントのエラーハンドリング漏れを拾ってくれた。
本番デプロイとnodejs_compat問題
Cloudflare Workers nodejs_compat
本番デプロイ時、Cloudflare Workersでnodejs_compatフラグが有効になっていないことが原因でBetter Authの依存パッケージがクラッシュした。Node.jsのビルトインモジュール(crypto等)をWorkersランタイムで使うには、wrangler.tomlにcompatibility_flags = ["nodejs_compat"]を明示的に追加する必要がある。
# wrangler.toml
compatibility_flags = ["nodejs_compat"]
この1行がないとデプロイは成功するがランタイムでエラーになる。ビルド時にはエラーが出ないので、実際にリクエストを飛ばして初めて気づく類の問題だった。
ローカルストレージからD1への移行フロー
未ログイン時にローカルストレージに溜まった学習履歴を、ログイン後にD1へ移行するフローを実装した。ログイン成功時にlocalStorageから履歴を読み出し、サーバーAPIにバルクPOSTで送信する。送信成功後にローカルの履歴をクリアする。
このフローがないと、ログイン前に解いた問題の履歴が消えてしまい、ユーザー体験を損なう。Progressive Engagementの設計では「ログイン前の行動を無駄にしない」のが肝になる。
今日の学び
- Googleログイン一本に絞ると、メール配信インフラが不要になり、固定費と実装範囲の両方が縮む。ユーザー数が読めない段階では選択肢を減らすことが正解だった
nodejs_compatフラグはビルド時にチェックされない。デプロイ後にランタイムで落ちるので、Node.jsのビルトインモジュールを使うパッケージを導入したら、まずwrangler.tomlを確認する手順を体に染み込ませる- ローカルストレージ→D1の移行フローは、ログイン後の初回リクエストに組み込むのが自然。ユーザーに「データ移行中」を意識させない設計にした
- 復習フィルターの粒度は、使ってみて初めて「もっと細かく分けたい」と感じる。最初からMAX細かくするのではなく、使いながら刻んでいく方が的確な分割になる
- Codexのコードレビューはファイル横断の整合性チェックに向いている。手で見ると見落としがちなcomposable間の型不整合を拾ってくれた
次のステップ
- Stripe審査通過後に買い切り決済を実装する
- 学習進捗のダッシュボード表示(管理画面側)
- Codex P2/P3指摘への対応
- 残りの章の問題データ移行と動作確認