ワダチを作る #12:ログイン・招待制・管理画面・退会…ハコヅミの土台を移して、マルチユーザーにした
前回(#11)で、ステップ3(過去の実績の表示)を本番に出しました。今回は、ステップ4:マルチユーザー対応です。ハコヅミで作った土台を、ワダチに移しました。
- Google でログインできるようにした(今のデータは、そのまま)
- セキュリティの決まり・管理画面・招待制を入れた
- アカウントの画面・データの書き出し・退会を作った
- 登録数の上限を、「守り」として入れた(有料版は、今は考えない)
- ここまでを、本番に出した
ステップ4を 10 の Issue に分けた
ステップ3を閉じて、ハコヅミの「マルチユーザーの土台」を、ワダチに移す Issue を 10 個作りました。先に、次のことを決めました。
- ログインは Google だけ
- 招待制にする(ベータ版のあいだは、招待した人だけが使える)
- 手元の開発サーバーで、ログインなしで入れる仕組みは、残す
- 登録数の上限は、習慣・内容・アイコンだけ(毎日の記録には付けない)
今回は、そのうち 8 つ(ログイン・セキュリティ・管理画面・招待制・アカウント・書き出し・退会・上限)を作って、本番に出しました。残りは、利用規約などと、公開の準備です。
ログイン:今のデータを、Google のアカウントにつなぐ
これまでは、ログインの代わりに、Cloudflare Access が渡すメールアドレスで、利用者を決めていました。そのとき、利用者のテーブル(users)を、ログインの仕組み(Better Auth)と同じ形で作っておきました。
その甲斐があって、同じメールアドレスの Google アカウントでログインすると、今の行にそのままつながり、習慣と記録が、そのまま見えます。ハコヅミでは、固定の値で始めて、あとから付け替える作業がありましたが、ワダチでは要りませんでした。「最初からマルチユーザーで作る」と決めた効果です。
結合テストで、穴が見つかった
つなぐ条件を作るとき、最初は「Google を信用する」設定(trustedProviders)を入れていました。ところが、Better Auth を実際に動かす結合テストを書いて試すと、この設定では、Google が確認済みとしていないメールアドレスでも、今の行につながってしまうことが分かりました。他人が、同じメールアドレスで Google アカウントを作れば、今のデータを見られてしまいます。
設定を外し、代わりに、今ある行を「確認済み」にするマイグレーションを足しました(Access が、メールアドレスの持ち主を確かめていたので)。Google の画面は自動テストでは通せませんが、Google から戻ってくるところは、Google のトークンの窓口だけを差し替えて、本物の Better Auth で確かめられます。
使っていない Better Auth の入口(ユーザー情報の更新など)は、全部 404 にしました。通すのは、Google から戻ってくる先と、エラーのページだけです。
招待制:仮の値のまま、マージされた
招待の一覧は、Cloudflare の Workers KV に、invite:<メールアドレス> の形で置きます。確かめるのは、新しくユーザーを作るときだけ。今いる人は、一覧になくても使い続けられます。
KV の置き場は、人がダッシュボードで作ります。そこで、ID を仮の値にして PR を出し、「マージの前に ID を教えてください」と書きました。ところが、仮の値のままマージされ、開発環境のデプロイが失敗しました。デプロイの手前で止まったので、動いている開発環境には影響なく、ID を入れる PR で直しました。
次の退会(同じく KV を使う)では、ID がそろうまで、PR を下書き(Draft)にしておくことにしました。書いて頼むより、押せない形にするほうが確かです。
断られることを、どう確かめるか
開発環境で「招待していない人は断られる」を確かめるには、まだユーザーがいない Google アカウントが要ります。手持ちの 2 つは、どちらもユーザーがいました。データの少ない方の行を、開発用のデータベースから消してもらい、「断られる → 招待に足す → 新しいユーザーができる」を通しました。

招待していないアカウントで、ログインしようとしたとき
招待制が入ったので、開発環境の Access を外しました。開発環境は、Google ログインと招待の一覧で守ります。本番の Access は、公開まで外しません。
セキュリティと、管理画面
セキュリティは、ハコヅミの決まりを、そのまま移しました。
- すべての応答に、セキュリティのヘッダー。CSP で、読み込めるのは自分のサイトと Google Fonts だけ(タイマーの音声は、ブラウザの中で作った
blob:を許す) - ほかのサイトからの送信は 403、大きすぎる送信は 413、長さの分からない送信は 411
- 送信の回数の制限(1 人 1 分 300 回、ログインまわりは 1 分 30 回)
回数の制限は、Cloudflare の Rate Limiting で数えます。数え方の番号は、アカウントの中で共有されるので、同じアカウントのハコヅミと同じ番号にすると、回数が合算されます。ワダチは、別の番号にしました。
管理画面は、管理者が、ユーザーの一覧と件数を見て、問題のある人を凍結できるものです。見えるのは件数までで、習慣の名前などの中身は出しません。作っている途中、手元の E2E のデータ(約 4,700 人)を 1 ページに出したら、アプリが落ちました。テストを流すたびにユーザーが増え続けていたのです。1 ページ 100 人に分けました。
アカウント・書き出し・退会
アカウントの画面では、表示名を決められます(将来の共有で、ほかの人に見える名前)。困ったのは、ヘッダーです。スマホ(320px)で、ロゴ・「記録」「習慣」・表示名を 1 行に入れられません。実装の前に 3 つの案を並べて、ハコヅミと同じ、右上に表示名の札、スマホでは 2 行にしました。
データの書き出しは、ZIP(全部の JSON、Excel で読める CSV、アイコン)です。サーバーは JSON を返すだけで、アイコンはブラウザが 1 枚ずつ取り、ブラウザの中で ZIP にまとめます(Workers のメモリには限りがあるため)。CSV は、= などで始まる文字の先頭に ' を付けて、表計算ソフトで式として動かないようにしています。
退会は、「退会する」と入力するまでボタンが押せない形です。消える順番は、招待の一覧 → データベース → 退会した人の記録。退会した人の id と日時だけを、7 日のあいだ KV に置きます。データベースを過去の時点に戻すと、そのあいだに退会した人のデータが戻ってしまうので、それを消し直すためです。
書き出しと退会は、利用者のデータのテーブルの一覧を 1 か所に置いて、どちらも同じ一覧を使います。テーブルを足して一覧に入れ忘れると、テストと型が見つけてくれます。
登録数の上限を、「守り」と決めた
ハコヅミには、会員区分(フリー/有料)と、登録数の上限があります。同じものを移すとき、上限の数を決める段で、相談になりました。
フリーと有料で差を出すとしたら、数になると思うが、上限までしか登録できないとなると、習慣トラッカーとしては致命的な気がする。逆に、上限を大きくすると、有料の意味も薄れそう。収益化があまり見込めないのであれば、いっそ有料版を考えないのも手。
整理すると、こうでした。
- 上限で止まるのは、習慣を新しく作ることだけ(習慣・内容の選択肢・アイコンの数)。「やった」の記録には、上限がない。10 年続けても、記録は止まらない
- 記録の 1 行はとても小さい。重いのはアイコンで、データベースの大きさに効く
そこで、**上限は、乱用でデータベースを埋められないための「守り」**にして、普通の使い方では当たらない数(習慣 100・内容 200・アイコン 300)にしました。有料版は、今は考えません。数で差を付けると、続けている人ほど困る形になり、「積み上がった跡は消えない」という方針とも合わないためです。作るなら、数ではなく、機能(本物の環境音、背景など)で差を付けます。
このとき、ひとつ、うっかりがありました。数を決め直す前に PR がマージされ、そのあとに同じブランチへ直しを push したので、直しが入りませんでした(前にも、同じ形のうっかりがありました)。開発環境のデータベースには、案の数のマイグレーションが当たっていたので、それは変えずに、新しいマイグレーションで直しました。push の前に、PR の状態を確かめる。もう一度、肝に銘じました。
本番に出した
8 つの Issue を、まとめて本番に出しました。マイグレーションは 6 つ。マージの前に、本番用の Google のログインの設定と、秘密の値を入れてもらいました(入れる前に出すと、本番にログインできなくなるため)。
デプロイは成功し、本番で、Google でログインして、今の習慣と記録が、そのまま見えました。最初の管理者は、人が、本番のデータベースのコンソールで決めました。
ちなみに、作業の途中で、使っていた Claude のモデルが、前日から Opus ではなく Sonnet に切り替わっていたことに気づき、戻しました。
おわりに
- 最初からマルチユーザーで作っておいたので、ログインを入れても、データの付け替えが要らなかった
- 「つながる・つながらない」は、本物を動かす結合テストで確かめる。設定を読むだけでは、穴に気づけなかった
- 人の作業が要る値は、書いて頼むより、下書きにして押せない形にする
- 登録数の上限は、何を守るためのものかから決めた。収益の線引きにはしない
- push の前に、PR の状態を確かめる
次回は、ステップ4の残り(利用規約・プライバシーポリシーと、公開の準備)か、ステップ5(環境音の見直しなど)に進みます。