• #eurekapu
  • #dashboard
  • #nuxt4
  • #cloudflare-pages
  • #d1
  • #better-auth
  • #管理画面
  • #設計変更
  • #日記
eurekapuメモ

独立型ダッシュボードを作って壊した日 — 1ドメイン統合型への方針転換

朝、管理画面をどこに置くかでClaude Codeと30分ほど議論を重ねた。同じアプリ内に埋め込むか、別リポジトリに切り出すか。「先生が複数アプリの学習状況を一画面で見たい」というユースケースを口に出した瞬間、答えが決まった。独立アプリにする。eurekapu-nuxt4/eurekapu-dashboard/ に配置して、tax-lpと同じパターンでモノレポの一部として管理することにした。

設計議論: SaaS時代の終焉とローカルファースト

議論の途中で話が広がり、「そもそもSaaS月額モデルはこの時代に合うのか」という問いが出てきた。Claude Codeのようなツールで対話しながらアプリを組み立てられる今、テンプレートを渡して各自がローカルで立ち上げる方が理にかなう場面がある。結果として、テンプレート提供(B2B軸)とコンテンツ提供(B2C軸)の2軸に整理した。具体的な販売戦略はまだ固まっていないが、頭の中のモヤモヤが構造として見える形になった。

技術構成

  • Nuxt 4 + Cloudflare Pages(SSR)
  • D1直接バインディング: API層を挟まず、問題集アプリのD1データベースを直接参照する。管理画面なのでトラフィックは少なく、抽象層を省いてシンプルに保つ判断
  • Better Auth: 認証レイヤー。管理者ホワイトリスト方式でアクセス制御

実装: 6フェーズ・全20ファイル

実装は6フェーズに分けて進めた。

  1. プロジェクト初期化 -- package.jsonnuxt.config.tswrangler.tomlのスケルトン作成
  2. 認証レイヤー -- Better Auth導入、admin-guardミドルウェアで管理者以外をブロック
  3. レイアウトとログイン画面 -- サイドバー付きレイアウト、Googleログイン画面
  4. ダッシュボードAPI -- ユーザー一覧・ユーザー詳細・統計の3エンドポイント。D1に直接SQLを投げる
  5. ダッシュボードページ -- 概要ページ、ユーザー一覧、ユーザー詳細の3画面
  6. デプロイスクリプト -- PowerShellスクリプトでローカルビルド→Cloudflare Pagesデプロイ

全20ファイルを作成し、ビルドが通った。ここまでは順調だった。

本番デプロイで踏んだ3つの罠

ビルド成功からデプロイまでの間に、3件のトラブルが待っていた。

1. Better Authのセッションテーブルにtokenカラムが無い

デプロイ後にログインしようとすると、D1側でカラム不足エラーが出た。Better Authのマイグレーションが生成したスキーマにtokenカラムが含まれていなかった。D1コンソールから手動でALTER TABLE session ADD COLUMN token TEXTを実行して解決。マイグレーション生成を過信せず、実テーブルとスキーマの差分を目視で確認する癖をつける必要がある。

2. SSR時のuseFetchがCookieを転送しない

サーバーサイドレンダリング時、useFetchでAPIを叩くと認証Cookieが付与されず、常に未ログイン扱いになる問題に遭遇した。Nuxt 3/4ではSSR時にブラウザのCookieが自動転送されない。useRequestFetch()に切り替えることで、リクエストヘッダーからCookieを引き継げるようになった。

// NG: SSR時にCookieが飛ばない
const { data } = await useFetch('/api/dashboard/stats')

// OK: リクエストヘッダーを引き継ぐ
const requestFetch = useRequestFetch()
const { data } = await useFetch('/api/dashboard/stats', { $fetch: requestFetch })

この1行の差でログイン後の画面が真っ白になるかデータが表示されるかが分かれる。SSR + 認証の組み合わせでは必ず意識すべきポイントだった。

3. 管理者ホワイトリストにメールアドレスが未登録

ログインは成功するが管理画面に入れない。admin-guardミドルウェアが弾いていた。環境変数のADMIN_EMAILSに自分のメールアドレスを追加して解決。単純なミスだが、エラーメッセージが「Unauthorized」だけだったので、認証の問題かガードの問題かの切り分けに少し時間を使った。

サーバーレス vs トラディショナルバックエンドの整理

ダッシュボード実装を通じて、Cloudflare Workersのサーバーレスモデルとトラディショナルなバックエンド(Express/Railsなど常駐サーバー)の違いを改めて整理した。

  • Workers: CPU実行時間30秒制限、リクエスト単位の課金、コールドスタートがほぼゼロ
  • トラディショナル: 実行時間制限なし、常時起動コスト、長時間バッチ処理に向く

管理ダッシュボードのように「少数ユーザーが軽いクエリを投げる」用途にはWorkersが合う。逆に、全ユーザーの学習データを集計するバッチ処理を入れたくなったら、30秒制限にぶつかる可能性がある。その場合はCronトリガーで事前集計テーブルに書き出すアプローチが現実的。

マネタイゼーション計画の改訂

サブスクリプションモデルではなく、3段階の買い切りモデルに整理した。

  1. フリー: 一部コンテンツを無料公開
  2. ログイン済み: 学習進捗の保存が可能に
  3. 買い切り: 全コンテンツへのアクセス権を一括購入

月額課金は管理コストが高く、ユーザーにとっても「いつ解約するか」を気にさせてしまう。買い切りなら一度払えば終わり。このシンプルさが、個人開発のサポート負荷にも合っている。

動画ホスティング調査

コンテンツに動画を含める場合のホスティング先を比較調査した。

サービス特徴懸念
Cloudflare StreamCloudflareエコシステム内で完結従量課金が読みにくい
Bunny.netコスト効率が高い、CDN品質も良いCloudflareとの二重管理
Mux開発者体験が良い、APIが充実価格が高め

結論は出していないが、まずは動画なしでテキスト+画像ベースのコンテンツを優先し、動画は需要が見えてから導入する方針にした。

今日の学び

  • useRequestFetch()はSSR + 認証の組み合わせで必須。これを知らずにuseFetchだけで組むと、サーバーサイドでの認証状態が常に未ログインになり、原因特定に時間を持っていかれる
  • マイグレーション生成ツールの出力を鵜呑みにしない。本番のD1テーブル定義をPRAGMA table_info(session)で直接確認する手順を踏んでいれば、tokenカラム不足はデプロイ前に気づけた
  • 6フェーズ・20ファイルを一気に作り切るとビルドは通るが、本番環境固有の問題は踏むまでわからない。次回は「2ファイル作ってデプロイ」を繰り返す方がトラブルの切り分けが楽になる

方針転換: 独立型ダッシュボード → 1ドメイン統合型

ここまで作り上げたダッシュボードだが、同日中にマネタイズモデルとユーザー体験を掘り下げた結果、独立型をやめて1ドメイン統合型に転換する判断をした。

なぜ壊したか

サブドメイン分離を前提に設計を進めていたが、実際のユーザー導線を考えたときに3つの問題が浮かび上がった。

  1. セッションの分断 — サブドメイン間でCookieが共有されず、サイトを移動するたびに再ログインが必要になる
  2. 購入状態の把握困難 — 「自分は何を買ったか」を確認する場所がない
  3. 課金導線の分断 — ある科目を終えて次に進みたいとき、別サイトに飛ばされて再度購入フローを踏む

1ドメイン・1アプリならこれらはすべて消える。

新しい構成

eurekapu.com(1ドメイン・1アプリ)
├── /              ← 無料(トップ、紹介ページ)
├── /boki3/        ← 購入済み or サブスク会員のみ
├── /boki2/        ← 同上
├── /admin/        ← 管理者のみ(メールホワイトリスト)
└── /login         ← 認証

管理画面はアプリ内の /admin/ ルートとして構築する。DBも1つのD1に集約。独立ダッシュボードで得た技術知見(Better Auth認証、D1直接バインディング、SSR時のCookie転送)はそのまま /admin/ 実装に流用できる。

廃止対象

  • eurekapu-dashboard/ ディレクトリ
  • eurekapu-dashboard-db(D1)
  • ダッシュボード用の Cloudflare Pages プロジェクト

無駄だったか?

作ったコードは捨てるが、SSR + 認証のCookie転送問題、D1スキーマの手動確認の重要性、useRequestFetch() の必要性など、本番デプロイで踏んだ3つの罠から得た知見は /admin/ 構築時にそのまま活きる。プロトタイプとして意味のある1日だった。


次のステップ

  • eurekapu-dashboard ディレクトリおよび関連リソースの削除
  • eurekapu-nuxt4 に /admin/ ルートを構築(ダッシュボードの機能を移植)
  • 購入管理テーブル(purchase, subscription)の追加
  • ルートベースのアクセス制御ミドルウェアの実装
  • 決済連携の検討(Stripe Checkout)