利用規約・プライバシーポリシー同意の記録
新規登録経路で利用規約・プライバシーポリシーへの同意を記録し、将来の「文書のバージョン管理」「更新時の再同意」まで見越した設計ドキュメント。
背景と現状の gap
ユーザーストアは実質 2 系統あり、同意の扱いがバラバラ。
| サービス | User ストア | 規約同意 | プラポリ同意 | バージョン |
|---|---|---|---|---|
| UZU mobile | Go backend app.users | なし | なし | なし |
| Studio | Go backend app.users(共有) | studio_terms_accepted_at 等(timestamp) | なし | なし |
| mdms.jp consumer / shop | mdms_jp Prisma User | termsAcceptedAt(初回予約時にセット) | なし | なし |
共通の問題:
- 記録が
*_accepted_atの timestamp 1 本で、「どの文書のどの版に同意したか」が残らない → 再同意判定が原理的にできない。 - プラポリ同意が一切記録されていない。
- mdms.jp は同意取得が「登録」ではなく「初回予約」に紐付く。
前提となる事実
- Firebase は全サービス共通(
uzu-dev/uzu-stg/uzu-prd)。firebase_uidは同一人物で一致する共通キー。 - apps/auth は全クライアント共通の認可サーバ。全新規ユーザーが必ず通る唯一の関所であり、全プロダクトが既に apps/auth に依存している。
- apps/auth は OAuth 認可サーバ(
/api/oauth/*・/api/callback、verifyIdToken/createCustomToken)。@uzupj/authの signup コンポーネントは既に規約同意チェックボックスを持つ。 - apps/auth の現状の永続化は Redis(TTL 付き短命データ)のみ。durable な store は持たない。
- Go backend は durable な User ストア(
appDB)。通信の向きは Go backend → mdms.jp(逆向きは無い)。
設計判断: apps/auth を同意の単一データソースにする
検討した代替案(各プロダクト DB に分散記録 / Go backend に集約)に対し、apps/auth を consent の単一データソースとする方針を採る。
理由:
- 依存方向を増やさない: 全プロダクトは認証で既に apps/auth に依存している。consent API を叩くのは同じ向きの依存に乗るだけ。Go backend 集約案は「apps/auth → Go backend」「mdms.jp → Go backend」という新しい向きを生む。
- 概念的に正しい: consent は OAuth 認可サーバの関心事(consent screen そのもの)。identity の層にデータが乗る。
- 「両 DB に記録するか」問題が消える: プロダクト DB(Go backend / mdms.jp)には consent を一切持たせない。単一データソース・単一 API・単一カタログが apps/auth に集約。
例外: 有償公開まわりの規約は Go backend が持つ
アカウントの利用に必要な規約(uzu_terms / studio_terms / mdmsjp_terms / privacy)は上記のとおり apps/auth が持つ。一方で、有償公開規約と追加コンテンツ有償公開規約は Go backend の user_terms_agreements が持つ。
この 2 つは分配金の算定条件そのものを定めており、同意の有無が scenario_members の分配率編集可否や有償公開の申込可否を直接ゲートする。判定が収益計算と同じトランザクション境界にあるため、別サービスへの往復を挟まない。
台帳の形((user_id, kind, version) の追記専用・一意制約で冪等)は apps/auth 側と揃えてある。将来 apps/auth に寄せる場合も、版付きで記録済みのため移行はデータの移送で済む。
トレードオフ: apps/auth は意図的に「TTL Redis しか持たない」設計だったため、durable store を新設する必要がある。
設計原則
- 同意は 「
firebase_uid×document_key×version」の追記専用ログとして残す。 - 台帳のアンカーは
firebase_uid(realm 横断で「同じ人」を指せる)。 - 収集と記録を分離する: 収集 = apps/auth(
@uzupj/auth)+ レイヤー文書は各プロダクト内。記録・データソース = apps/auth の durable store。 - 「サービス」は要件ゲートだけを持つ。同意台帳はアカウント単位でグローバル。サービスを跨ぐ=同じ台帳に対する要件の再評価。
- version を初日から保持し、バージョン管理・再同意は移行ゼロで後付けできる形にする。
データモデル: 同意台帳
LegalConsent(追記専用・更新も削除もしない / apps/auth が所有)
────────────────────────────────────────────
firebase_uid … 人物アンカー(全サービス共通キー)
document_key … 利用規約=サービスごと "uzu_terms" | "studio_terms" | "mdmsjp_terms"
プラポリ=全社共通 1 本 "privacy"
version … 各文書の日付(例 uzu_terms は "2024-11-22"・サーバが付与)
consented_at … 同意日時
source … 経路 "auth_signup" | "mobile_reconsent"
| "mdmsjp_signup" | "studio" …
────────────────────────────────────────────
一意性 (firebase_uid, document_key, version) ← 冪等・二重記録防止- 既存の
studio_terms_accepted_at/ mdms.jptermsAcceptedAtは apps/auth store へ v1 として backfillし、以後はプロダクト DB ではなく apps/auth から read。プロダクト側のカラムは移行後 deprecate。 studio_paid_release_terms_accepted_at/studio_additional_content_terms_accepted_atは上記の例外にあたり、Go backend のuser_terms_agreementsへ移行する。- store は Postgres 採用。物理配置は本ドキュメント「インフラ (Postgres 相乗り)」節を参照。
documentKey とバージョンカタログ(apps/auth が保持)
「現行版」はまず apps/auth のコード定数で持つ(Phase 1 では DB 化しない)。
CurrentLegalVersions = {
// 利用規約 = サービスごと
uzu_terms: { version: "2024-11-22", requiresReconsent: true, effectiveAt: "2024-11-22" }, // 最新改訂日
studio_terms: { version: "2021-07-24", requiresReconsent: true, effectiveAt: "2021-07-24" }, // 制定日
mdmsjp_terms: { version: "2024-10-08", requiresReconsent: true, effectiveAt: "2024-10-08" }, // 日付記載無→サービス公開日
// プラポリ = 全社共通 1 本(現状 uzu のものを参照、後で差し替え可能)
privacy: { version: "2026-07-22", requiresReconsent: true, effectiveAt: "2026-07-22" }, // 制定 2021-04-21・2026-07-22 全面改定
}- version はクライアントに渡させず、apps/auth がカタログから刻む(古い版番号を送ってくる事故を防ぐ)。クライアントは「どの documentKey に同意したか」だけ送る。
- 各 version は
requiresReconsent(実質変更か軽微修正か)とeffectiveAtを持つ。判定ロジックの単一ソースが apps/auth。 - プラポリ差し替え:
privacyの documentKey は不変のまま、contentRef(本文の参照先)とversionを切り替えるだけで差し替えられる。実質変更ならrequiresReconsent: trueの新 version にすれば全ユーザー再同意が自動でかかる。
文書の構成
- 利用規約 = サービスごと:
uzu_terms(uzu app/mobile)/studio_terms(Studio)/mdmsjp_terms(mdms.jp)。収集は各サービスの登録経路。 - プライバシーポリシー = 全社共通 1 本:
privacy。現状 uzu のプラポリを参照し、後で差し替え可能にする。firebase_uidで 一度同意すれば全サービスで有効(跨いでも再同意不要)。 studio_paid_release_terms/studio_additional_content_termsは利用規約とは別のレイヤー契約(従来通り)。
documentKey × サービス必須マトリクス
| 入口 | 必須 documentKey |
|---|---|
| uzu mobile | uzu_terms, privacy |
| studio | studio_terms, privacy |
| mdms.jp consumer / shop | mdmsjp_terms, privacy |
実装は client_id → 必須 documentKey のスコープマッピング(apps/auth src/lib/clients.ts の getRequiredDocumentKeys)。未指定 / 未登録の client_id は uzu_terms + privacy に倒す。
例: mobile で登録(uzu_terms + privacy に同意)したユーザーが後で mdms.jp に来た場合、privacy は共通で同意済みなので mdmsjp_terms だけ要求される。
アーキテクチャ
apps/auth が consent の単一データソース
├─ store : Postgres(AlloyDB uzu-shared に相乗り)。追記専用
├─ カタログ : 現行 version を保持(単一ソース)
└─ API
POST /api/callback signup 時は consented documentKeys を受領し append(実装済み)
POST /api/consent accept(token 検証 → version 付与 → append)※mobile 直叩き・レイヤー文書用
GET /api/consent/pending pending 判定 ※将来(再同意ゲート用)
全プロダクト(mobile native / studio / mdms.jp)→ apps/auth API を叩く(既存依存に同居)
プロダクト DB には consent を持たせない| 役割 | 置き場所 |
|---|---|
| 収集(UI・チェックボックス) | apps/auth(@uzupj/auth)+ レイヤー文書は各プロダクト内 |
| 記録・データソース | apps/auth の durable store |
| 現行版・判定ロジック | apps/auth(カタログ単一ソース) |
| enforcement(ゲート) | 今回未実装(将来)。各サービスがトークンのクレーム//api/consent/pending で判定 |
token クレームによる steady-state 最適化(将来拡張・今回未実装)
apps/auth は token 発行者なので、発行する custom token / session に consent 充足クレーム(例: consent_ok や同意済み版)を埋められる。
- 通常時のゲートはトークンのクレームを見るだけ= apps/auth への追加 API 呼び出し不要。mdms.jp backend も Firebase トークン検証時にクレームを読むだけで済む(逆向き依存ゼロ)。
- クレームは TTL(〜1h)で stale になる。利用中の version 上げ(再同意)の鮮度判定だけ
GET /api/consent/pendingを authority として叩く。
記録フロー(新規登録)
全クライアント
↓ apps/auth /login?client_id=...(mobile は webview で開く)
@uzupj/auth: サービス利用規約 + プラポリの同意チェックボックス(収集)
│ 必須 documentKey は client_id → スコープマッピング(apps/auth lib/clients.ts の
│ getRequiredDocumentKeys)で解決。リンク先は /legal/[documentKey]/latest
↓ 認証成功(Firebase isNewUser = 新規登録のときのみ)
apps/auth /api/callback(firebase_uid 確定・consented_document_keys を受領)
│ version は送らせず、client_id の必須 documentKey が揃っていることを検証
↓ recordConsents(/api/consent と同じロジックを同一プロセスで実行)
apps/auth store に append(記録に失敗したら新規登録を完了させない)レイヤー文書(studio_paid_release_terms 等)は各プロダクト内で収集し、POST /api/consent で append。
再同意の表示ロジック(将来拡張・今回未実装)
WARNING
本節は「文書のバージョン管理と更新時の再同意」を見越した設計であり、今回の実装スコープ外。今回は新規登録時に現行版の同意を記録するところまで。version は同意レコードに残すため、将来この節を実装しても移行は不要。
バージョン判定規則(apps/auth に集約)
currentRequiredVersion(key)=requiresReconsent=trueの最新版(軽微修正の版は対象にしない)。isConsented(uid, key)= 台帳に(uid, key, v)が存在しv >= currentRequiredVersion(key)を満たす。- 軽微修正(
requiresReconsent=false)の版上げはcurrentRequiredVersionを動かさない → 全ユーザーに無意味な再同意を出さない。
クライアントはカタログを持たず、結果だけ受け取る。
pending 判定 API(apps/auth)
GET /api/consent/pending?scope=studio
→ [{ documentKey, version, effectiveAt, summary, contentRef }]「scope に含まれる documentKey のうち isConsented=false のもの」を返す。空配列ならゲート通過。
トリガーは 2 系統(両方必要)
ログイン時だけでは不十分(mobile は再ログインが稀で、version は利用中にも publish されるため)。
- ログイン時(apps/auth):
/api/callbackで base 文書の pending を確認 → あれば consent screen を出してからリダイレクト。 - 利用中(各プロダクトの gate): アプリ起動時・トークン refresh 時に
/api/consent/pendingを叩く → 非空なら blocking modal をオーバーレイ。- mobile は webview にバウンスせず native で
POST /api/consentを直叩き(Firebase トークンを持っているため)。
- mobile は webview にバウンスせず native で
アプリ起動 / トークンrefresh
↓
GET /api/consent/pending(scope)
↓ 非空
blocking modal(更新告知 + summary + effectiveAt + 同意ボタン)
↓ accept
POST /api/consent(documentKeys) → 台帳 append(version は apps/auth 付与)
↓ refetch → 空になったら unblock表示内容と決定事項
- 表示: 「規約を更新しました」+ 主な変更点(
summary)+ 発効日(effectiveAt)+ 本文リンク + 同意ボタン。 - 複数 pending(規約とプラポリ同時など)はまとめて表示し一括 accept。
- decline の挙動(同意せず続行不可 → ログアウト/機能制限)は product / 法務判断。
- 初回同意・再同意・軽微更新の区別は判定規則が吸収するので、UI 側は「pending があるか」だけ見ればよい。
インフラ (Postgres 相乗り)
store は Postgres 採用。新インスタンスは立てず、既存 AlloyDB / Cloud SQL に新規 database を相乗りさせる。
環境ごとの相乗り先
| 環境 | インスタンス | apps/auth の DB |
|---|---|---|
| prd | AlloyDB uzu-shared クラスタ(uzu-shared-primary, 10.68.146.6, 2CPU) | 新規 database auth |
| stg | AlloyDB uzu-shared クラスタ | 新規 database auth |
| dev | Cloud SQL uzu-postgresql(Postgres 16、既に 6 DB 相乗り) | 新規 database auth |
backend のコア app(prd: AlloyDB uzu-app クラスタ, 16CPU)には載せない。 uzu-shared は既に mdms_jp が同居する「非コア/共有 DB の同居先」であり、既存パターンに一致し、コア app DB への blast radius を避けられる。
相乗りの実務条件
- 専用 database + 最小権限ユーザー:
app/mdms_jpとスキーマを混ぜず独立 database。postgressuperuser ではなく apps/auth 専用ロールを切り、その DB のみに権限付与。 - 接続情報: apps/auth の k8s に
DATABASE_URLを external-secrets 経由で追加(現状 apps/auth は Redis + firebase-admin のみ)。 - マイグレーション: apps/auth 側に Prisma を導入(mdms.jp と同じスタックで一貫)。database / ユーザー作成は手動でなく terraform / migration job で(drift 回避)。
- 容量:
uzu-shared-primaryは 2CPU。consent は追記中心の低負荷で実質無視できるが、接続プール上限は infra オーナーに確認(必要なら CPU 増)。
決定事項
| 論点 | 決定 |
|---|---|
| 実装スコープ | 新規登録時の同意記録まで。再同意・バージョン管理ゲート・decline 挙動は今回未実装(設計のみ将来拡張) |
| 利用規約 | サービスごと(uzu_terms / studio_terms / mdmsjp_terms) |
| プライバシーポリシー | 全社共通 1 本(privacy、現状 uzu のものを参照・差し替え可能) |
| プラポリの扱い | 登録時に「同意」として台帳に記録(UI はチェックボックス)。再同意ゲートは今回未実装 |
| store | Postgres(新インスタンスを立てず AlloyDB uzu-shared / dev は uzu-postgresql に相乗り) |
| 記録に残す version | 各同意に document_key + version を残す(将来のバージョン管理の布石。判定ロジックは今回実装しない) |
| mdms.jp の取得タイミング | 新規登録は apps/auth 経路で mdmsjp_terms + privacy を台帳へ記録。既存ユーザーの初回予約時 termsAcceptedAt はレガシー据置(撤去しない) |
| 既存ユーザー / バックフィル | 記録対象外・backfill しない。記録開始日(収集デプロイ日)以降の新規登録アカウントのみ台帳に載る |
補足
- カットオフは実装上自然に成立: 同意記録は登録フロー(apps/auth signup)でしか書かれないため、収集デプロイ日以降の新規登録アカウントのみ台帳に載り、既存アカウントは一切変更されない。明示的な日付フィルタ実装は不要。
- 再同意・decline=ログアウトは将来の再同意ゲート導入時に有効化する設計(本ドキュメント「再同意の表示ロジック」節)。今回は実装しない。