認証と管理機能のアーキテクチャ
このテンプレートにおける Better Auth のセッション、OAuth、RBAC、そしてクレジット/クォータの業務データ設計を詳しく解説します。
認証と Admin
このドキュメントは、このテンプレートの認証と管理者認可の仕組みを理解するための基準ドキュメントです。
以下を扱います。
- このプロジェクトで実際に使っている Better Auth モデル
- メール/パスワードと Google OAuth がどうセッションを維持するか
users/accounts/sessions/verificationsを分ける理由- 認証データと業務データ(クレジット、クォータ、権限)の境界
- このコードベースでの Admin RBAC 実装
1) このテンプレートの認証モデル
このプロジェクトは、Postgres をバックエンドにした Cookie ベースのサーバー管理セッションを使います。 デフォルトでは JWT-first ではありません。
根拠:
src/lib/auth.tsで Better Auth を Drizzle + Postgres アダプタで設定- セッション保存先は
src/db/schema.tsのsessionsテーブル nextCookies()プラグインを有効化- JWT を主方式にするプラグインは有効化していない
実際の挙動:
- ブラウザは HttpOnly の認証 Cookie(セッショントークン)を保持
- サーバーはそのトークンを
sessionsと照合 - セッション行からユーザーを特定
Better Auth は JWT もサポートしますが opt-in であり、このテンプレートの DB セッション方式を置き換えるものではありません。
2) 認証テーブルの役割分担
Better Auth は、アイデンティティ、認証手段、ログイン状態を意図的に分離します。
users(ユーザープロファイル)
その人が誰かを表すテーブルです。
代表的な項目:
- プロファイル(
nickname,avatar_url) - ロール(
role) - 検証/状態フラグ
- 作成/更新時刻
このテンプレートでは追加で:
uuid(作成時に付与)role
users は高頻度更新の業務カウンタを入れず、安定させるのが基本です。
accounts(認証手段)
どうログインするかを表すテーブルです。
- 1ユーザーが複数のプロバイダを持てる
- プロバイダ識別子とトークンを保持
代表的な項目:
provider_idaccount_id- パスワードハッシュ(メール/パスワード)
- OAuth の access token / refresh token
- OAuth トークン有効期限
資格情報を扱うため、特にセキュリティ重要度が高いテーブルです。
sessions(ログイン状態)
アプリ内のアクティブなログインセッションを表します。
代表的な項目:
user_idtokenexpires_atip_addressuser_agent
性質:
- セッション有効期限がログイン状態を決める
- 失効は行の削除/無効化で行う
- スライディング更新はセッションポリシー依存
verifications(一時的な検証フロー)
以下のような一時トークン用途:
- パスワードリセット
- メール検証
- マジックリンク / OTP(有効時)
通常のサインイン/サインアウトでは継続的に増えないため、行数が少ないのは正常です。
3) このコードベースでの認証フロー(エンドツーエンド)
3.1 メール + パスワード サインアップ
- クライアントがサインアップフォーム送信
- Better Auth が
users行を作成 src/lib/auth.tsのdatabaseHooks.user.create.beforeでuuidを保証- Better Auth がメール/パスワード用
accounts行を作成 - Better Auth が
sessions行を作成 - レスポンスでセッション Cookie を設定
databaseHooks.user.create.afterがウェルカムメール送信を非同期実行
結果: ユーザーは即時ログイン状態になります。
3.2 メール + パスワード サインイン
- クライアントが資格情報を送信
- Better Auth が
accountsのパスワードハッシュを検証 - Better Auth が
sessionsを作成/更新 - セッション Cookie を設定/更新
3.3 Google OAuth サインイン
- ユーザーが Google サインインをクリック
- OAuth のリダイレクト + コールバックが実行
- Google からアカウント識別子(
sub)とプロフィール情報を取得 - Better Auth がユーザーを検索または作成
- Better Auth が
google用accounts行を作成/更新 - Better Auth がセッション行を作成
- セッション Cookie を設定
4) OAuth アイデンティティの重要ルール
Google サインインは Gmail アドレス必須ではありません。
- Gmail、Outlook、Yahoo、独自ドメインでも Google アカウントなら利用可能
- 同一性はメールドメインではなく、プロバイダのアカウント識別子(
provider_id,account_id/sub)で判断
メールドメインをプロバイダ証明として扱わないでください。
5) セッション期限と OAuth トークン期限の違い
この2つは独立したライフサイクルです。
アプリセッション(sessions)
- アプリ内でログイン済みかを決める
- 期限切れなら
getSession()は null - ユーザーは再ログインが必要
プロバイダトークン(accounts)
- バックエンドがプロバイダ API を叩けるかを決める
- access token は短命
- refresh token で再発行可能
重要ポイント:
- Better Auth のセッション失効で、Google refresh token を使って自動再ログインはしない
- refresh token はプロバイダ API アクセス用途であり、アプリ認証用途ではない
6) セッションと OAuth トークンを分ける理由
この分離は安全性と運用性のために必須です。
- OAuth refresh token はアプリの本人証明ではない
- セッション失効を意味あるものにできる
- 明示的な再認証を維持できる
- セキュリティ監査境界が明確になる
refresh token から自動的にアプリセッションを再作成すると、失効管理と監査性が弱くなります。
7) このテンプレートの認可(RBAC)
認証は「誰か」を判定し、認可は「何ができるか」を判定します。
ロールは users.role で管理:
useradmin_roadmin_rw
ロールチェック実装: src/lib/authz.ts
requireAdminRead()->admin_roとadmin_rwを許可requireAdminWrite()->admin_rwのみ許可
管理画面レイアウトガード: src/app/(admin)/layout.tsx
8) 現在の管理画面機能と API
Admin ページ
- ダッシュボード:
src/app/(admin)/admin/page.tsx - ユーザー一覧:
src/app/(admin)/admin/users/page.tsx - ユーザー詳細/クォータ/クレジット:
src/app/(admin)/admin/users/[uuid]/page.tsx - 注文一覧/ステータス:
src/app/(admin)/admin/orders/page.tsx - フィードバック:
src/app/(admin)/admin/feedbacks/page.tsx - 予約:
src/app/(admin)/admin/reservations/page.tsx - アフィリエイト:
src/app/(admin)/admin/affiliates/page.tsx
Admin API ルート
GET /api/admin/users-> ユーザー一覧(ページング)GET /api/admin/users/[uuid]-> ユーザー詳細 + 月次利用量 + クレジット概要PATCH /api/admin/users/[uuid]->monthlyCreditsQuota更新GET /api/admin/users/[uuid]/credits-> クレジット概要 + 台帳POST /api/admin/credits/grant-> クレジット付与POST /api/admin/credits/adjust-> クレジット増減調整GET /api/admin/orders-> ステータスフィルタ付き注文一覧(all|paid|created|deleted)
9) Cookie セッションと JWT の違い(現状)
現状の構成:
- セッション Cookie にセッショントークンを保持
- サーバーは
sessionsテーブルで検証 - DB セッション経由でユーザーを解決
つまり、DB バックドのセッション認証であり、純粋な stateless JWT 認証ではありません。
純粋 JWT モデル(現状のデフォルトではない):
- トークン自体に claims を保持
- 各リクエストで署名検証
- 基本検証に DB 参照が不要なことが多い
10) BETTER_AUTH_SECRET が必要な理由
セッショントークンがランダムでも、署名/検証の暗号処理のために secret は必要です。
Better Auth の secret 解決順:
- 設定の
secret BETTER_AUTH_SECRETAUTH_SECRET- 内部フォールバック(開発向け、運用非推奨)
そのため env に secret がなくても起動する場合がありますが、本番のセキュリティとしては不十分です。
環境ごとに強く安定した secret を設定してください。 secret を変更すると、署名済み認証情報が無効化されログアウトが発生することがあります。
11) 業務データ設計: 認証テーブルに入れないもの
高頻度更新の業務カウンタを認証テーブルへ混在させないでください。
認証テーブルは認証に限定
users: identity/profile/roleaccounts: ログインプロバイダ + 資格情報/トークンsessions: ログイン状態verifications: one-time 検証トークン
プロダクト/課金状態はドメインテーブルへ
推奨パターン:
- 現在状態テーブル(高速参照)
- 追記専用 ledger/event(監査・再計算)
- entitlement テーブル(機能許可)
構造例:
-
user_balanceuser_id(PK)credits_balancequota_limitquota_usedplan_idbilling_period_startbilling_period_endupdated_at
-
usage_eventsiduser_idtypeamountcost_creditsmetadata(JSON)created_at
-
entitlementsuser_idkeyvaluesource(plan/admin/promo)expires_at
この分離により、認証層は安定し、業務ロジックは監査可能になります。
12) クォータ/クレジット消費とトランザクション安全性
クレジット/クォータ消費時:
- DB トランザクションを使う
- 残高/クォータを確認
- 利用イベントを ledger に追加
- 条件付きで残高更新
これにより並行実行時の race condition やマイナス残高を防げます。
このテンプレートでは:
- タスク利用量は
tasks - クレジット台帳は
credits - 月次クォータ(
users.monthly_credits_quota)を消費前にチェック
13) 将来拡張: Organizations
チーム向けを想定するなら:
organizationsを追加- 残高/クォータ/entitlement を組織単位へ移動
- 個人アカウントは 1人組織として扱う
早めにこのモデルを前提化すると、後の大規模リファクタを避けられます。
14) 環境変数とセットアップチェックリスト
最低限必要な認証関連 env:
BETTER_AUTH_URLNEXT_PUBLIC_AUTH_BASE_URLBETTER_AUTH_SECRET(強いランダム値)GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET(Google 有効時)
スキーマ変更時はマイグレーション実行:
pnpm drizzle-kit generate --config src/db/config.ts
pnpm drizzle-kit migrate --config src/db/config.ts最終まとめ
- このプロジェクトは Cookie + DB セッション方式(JWT-first ではない)
- OAuth の同一性はメールドメインではなく provider account で扱う
- アプリセッションと provider token は別ライフサイクル
- 認証テーブルは auth 専用に保つ
- クレジット/クォータ/権限は ledger を伴うドメインテーブルへ分離
- Admin RBAC は DB ロールをサーバー側で厳格にガード
この構成により、強固なセキュリティ境界、運用しやすさ、将来の拡張性を同時に確保できます。