Secret Birthday - DB設計

2026-04-16 作成

1. プロジェクト全体像

3
アプリケーション
9
Firestoreコレクション
0
重複コレクション
0
非正規化フィールド

ステークホルダー階層

階層対象アプリ主な機能
L1 プラットフォーム Gonmura admin/gonmura (Next.js) 事務所管理、全体統計、システム設定
L2 事務所 芸能事務所等 admin/agency (Next.js) タレント管理、パスコード設定、収益確認
L3 配信者 タレント talent-app (KMP) 配信開始/終了、ビンゴ抽選、コメント確認
L4 視聴者 ファン src/ (React + Vite) ライブ視聴、ギフト、ビンゴ、質問投票

2. システム構成

Fan Web App

ファン向けWebアプリ

React 19 + Vite + TypeScript + Tailwind
  • ライブ視聴 (Amazon IVS)
  • ギフト送信 (8種 + カスタム)
  • コイン購入 (Stripe)
  • ビンゴゲーム (3x3)
  • 質問投票箱
  • パスコード認証(共通パスワード方式)
Agency Admin

事務所管理画面

Next.js 15 + TypeScript + Tailwind
  • タレント管理 (CRUD)
  • パスコード設定・変更
  • カスタムギフト設定
  • スタッフ管理 (4ロール)
  • 収益レポート
Gonmura Admin

プラットフォーム管理画面

Next.js 15 + TypeScript + Tailwind
  • 事務所アカウント発行
  • ユーザー管理・凍結
  • 配信管理・強制停止
  • 全体売上・IVSコスト
  • 監査ログ
  • システム設定

Backend: Firebase + 外部サービス

Cloud Firestore Firebase Auth Cloud Functions Cloud Storage FCM Push Amazon IVS Stripe Connect

3. 設計原則

正規化ルール

ルール説明
集計値・残高を保存しないCOUNT / SUM / 残高はサブコレクションから都度集計する
名前を埋め込まないuserId のみ保存し、表示時に users を参照する
定数を複製しないgiftName, price は GIFT_DEFINITIONS から導出
データの正本は1箇所同じデータを複数コレクションに持たない

導出パターン

必要な値導出方法
コイン残高SUM(users/{id}/transactions.coins WHERE status=completed) - SUM(giftHistory 価格)
配信者名users/{streamerId}.displayName
視聴者数COUNT(viewers WHERE isActive=true)
ギフト合計SUM(gifts → GIFT_DEFINITIONS[giftId].price)
ギフト名・価格GIFT_DEFINITIONS[giftId]
投票数COUNT(votes)
累計消費COUNT(giftHistory) × GIFT_DEFINITIONS[giftId].price
タレント数COUNT(agencies/streamers)

4. Firestore コレクション構造

全フィールドが正規化済み。集計値・名前の埋め込み・重複コレクションは全て排除。表示に必要な導出値はCloud Functions / フロントのクエリ時に算出する。
users/{userId} トップレベル
userIdstringドキュメントID(Firebase Auth UID)
emailstringメールアドレス
displayNamestring表示名
photoURLstringプロフィール画像
rolestringfan / streamer / agency / admin
fcmTokensstring[]プッシュ通知トークン
createdAtTimestamp
updatedAtTimestamp
giftHistory/{historyId}
historyIdstringドキュメントID
streamIdstring配信ID
giftTypestringギフト種別(1ドキュメント=1ギフト、価格は定数から導出)
createdAtTimestamp
updatedAtTimestamp
transactions/{transactionId} Stripe webhookで記録
transactionIdstringドキュメントID
typestringpurchase / refund / bonus
statusstringpending / completed / failed
packageIdstringコインパッケージID
coinsnumber取得コイン数(ボーナス含む)
amountnumber決済金額(円)
stripeSessionIdstringStripe Session / PaymentIntent ID
paymentMethodstringcard / apple_pay / google_pay
createdAtTimestamp
updatedAtTimestamp
completedAtTimestamp
streams/{streamId} トップレベル
streamIdstringドキュメントID
streamerIdstring配信者UID(users から名前を参照)
agencyIdstring所属事務所ID
titlestring配信タイトル
descriptionstring配信説明
statusstringscheduled / live / ended
scheduledAtTimestamp配信予定日時
playbackUrlstringIVS再生URL
channelArnstringIVSチャンネルARN
ingestEndpointstringIVS配信エンドポイント
streamKeystringIVS配信キー
createdAtTimestamp
updatedAtTimestamp
startedAtTimestamp配信開始日時
endedAtTimestamp配信終了日時
comments/{commentId}
commentIdstringドキュメントID
userIdstring投稿者UID(users から名前を参照)
contentstringコメント内容
typestringnormal / gift
giftTypestring?type=gift の場合のギフトID
createdAtTimestamp
updatedAtTimestamp
questions/{questionId}
questionIdstringドキュメントID
userIdstring質問者UID(匿名時は表示で隠す)
contentstring質問内容
isAnonymousboolean匿名フラグ
statusstringpending / approved / hidden / deleted
isAnsweredboolean
createdAtTimestamp
updatedAtTimestamp
votes/{userId}
userIdstringドキュメントID(投票者UID)
createdAtTimestamp
updatedAtTimestamp
bingo/current シングルトン
statusstringwaiting / active / ended
drawnNumbersnumber[]抽選済みの番号リスト
numberRangemap{ min: 1, max: 15 }
createdAtTimestamp
updatedAtTimestamp
startedAtTimestamp
endedAtTimestamp
winners/{winnerId}
winnerIdstringドキュメントID
userIdstring勝者UID(users から名前を参照)
ranknumber順位
bingoLinesnumberビンゴライン数
createdAtTimestamp
updatedAtTimestamp
bingoCards/{userId}
userIdstringドキュメントID(カード所有者UID)
numbersnumber[9]3x3グリッドの数字
markedbool[9]マーク済み状態
isBingobooleanビンゴ達成
bingoLinesnumber達成ライン数
createdAtTimestamp
updatedAtTimestamp
gifts/{giftId}
giftIdstringドキュメントID
userIdstring送信者UID(users から名前を参照)
giftTypestringギフト種別(1ドキュメント=1ギフト、価格は定数から導出)
createdAtTimestamp
updatedAtTimestamp
viewers/{userId}
userIdstringドキュメントID(視聴者UID)
isActiveboolean現在視聴中フラグ
joinedAtTimestamp入室日時
leftAtTimestamp?退室日時
updatedAtTimestamp
agencies/{agencyId} トップレベル
agencyIdstringドキュメントID
namestring事務所名
ownerUidstringオーナーのAuth UID
ownerEmailstringオーナーメール
statusstringactive / suspended / deleted
createdAtTimestamp
updatedAtTimestamp
streamers/{streamerId} 唯一のタレント正本
streamerIdstringドキュメントID(Firebase Auth UID)
emailstring
displayNamestring
photoURLstring
statusstringactive / inactive / deleted
passcodestring共通パスコード(事務所が設定・変更可能)
createdAtTimestamp
updatedAtTimestamp
staff/{staffUid}
staffUidstringドキュメントID(Firebase Auth UID)
emailstring
displayNamestring
agencyRolestringowner / manager / staff / accountant
statusstringactive / inactive / deleted
createdAtTimestamp
updatedAtTimestamp
custom_gifts/{giftId}
giftIdstringドキュメントID
namestringギフト名
pricenumber価格(コイン)
emojistring
imageUrlstring
descriptionstring
isActiveboolean
createdAtTimestamp
updatedAtTimestamp
notifications/{notificationId} トップレベル
notificationIdstringドキュメントID
userIdstring宛先ユーザー
titlestring
bodystring
datamapFCM用メタデータ
readboolean既読フラグ
createdAtTimestamp
updatedAtTimestamp
audit_logs/{logId} トップレベル
logIdstringドキュメントID
actorIdstring操作者UID
actorAgencyIdstring操作者の事務所ID
actionstring操作種別
resourcestring対象リソースパス
beforemap変更前データ
aftermap変更後データ
createdAtTimestamp追記のみ(書き換え不可)
system/settings シングルトン
maintenanceModeboolean
createdAtTimestamp
updatedAtTimestamp
updatedBystring

5. エンティティ関連図

/* ======================================== Secret Birthday - ER図 ======================================== */ agencies ─────┬── 1:N ──> agencies/streamers (タレント - 唯一の正本) │ │ └── passcode (共通パスワード - 1配信者1つ) │ ├── 1:N ──> agencies/staff (事務所スタッフ) │ └── 1:N ──> agencies/custom_gifts (カスタムギフト) │ └── ownerUid ──> users users ──────────┬── 1:N ──> users/giftHistory (ギフト送信履歴) └── 1:N ──> users/transactions (コイン取引履歴) streams ─────────┬── 1:N ──> streams/comments (コメント) │ ├── 1:N ──> streams/questions (質問) │ │ └── 1:N ──> votes (投票) │ ├── 1:1 ──> streams/bingo/current (ビンゴゲーム) │ │ └── 1:N ──> winners (勝者) │ ├── 1:N ──> streams/bingoCards (ビンゴカード) │ ├── 1:N ──> streams/gifts (ギフト) │ └── 1:N ──> streams/viewers (視聴者) │ ├── streamerId ──> users (配信者 = users の1人) └── agencyId ───> agencies (所属事務所) notifications └── userId ─────> users audit_logs ├── actorId ────> users └── actorAgencyId > agencies /* 削除されたコレクション: - streamers/{id} (トップレベル) → agencies/streamers に統合 - passcodes/{id} (トップレベル) → agencies/streamers.passcode に統合 - sessions/{id} → パスコード再設計で不要に - schedules/{id} → streams.scheduledAt で管理、別コレクション不要 - payments/{id} → users/{id}/transactions に統合(Stripe webhookで記録) - revenue_reports/{id} → streams/gifts から都度集計(事前集計は不要) - payouts/{id} → 事務所への送金フロー自体が不要になったため削除 */ /* 削除されたフィールド: - users.coins → SUM(transactions) - SUM(giftHistory の価格) で算出 - giftHistory.quantity → 1ドキュメント=1ギフト(常に1) - giftHistory.totalCost → GIFT_DEFINITIONS[giftId].price で導出 - gifts.quantity → 1ドキュメント=1ギフト(常に1) - gifts.totalCost → GIFT_DEFINITIONS[giftId].price で導出 - agencies.distributionRate → 固定値または外部設定で管理 - agencies.stripeAccountId / stripeAccountStatus → 事務所送金フロー廃止で不要 - system.platformFeeRate → 送金時の手数料計算が不要になったため削除 */

6. パスコード設計(共通パスワード方式)

1配信者 = 1パスコード

配信者A に対して:
└── パスコード "ABCD1234"(1つだけ)
    ├── ファン1 -> このコードで入室 OK
    ├── ファン2 -> 同じコードで入室 OK
    └── ファン3 -> 同じコードで入室 OK

    -> 事務所がいつでもコードを変更可能

データ保存先:
  agencies/{agencyId}/streamers/{streamerId}
    └── passcode: "ABCD1234"   (たった1フィールド)
    

パスコード検証フロー

Step 1
ファンがコード入力
->
Step 2
Cloud Function で照合
->
Step 3
streamers.passcode と比較
->
Step 4
一致 -> 視聴許可

事務所管理画面での操作

操作処理内容
パスコード確認 agencies/{id}/streamers/{id}.passcode を読み取って表示
パスコード変更 agencies/{id}/streamers/{id}.passcode を上書き更新
パスコード無効化 passcode フィールドを空文字 or 削除
旧設計からの変更点: passcodes コレクション(トップレベル + 事務所サブコレクション)を完全廃止。 sessions コレクションも不要に(同時アクセス制御はアカウント単位で viewers サブコレクションで管理)。 パスコード一括生成、CSV出力、usedBy 追跡は全て不要。

7. 主要データフロー

A. コイン購入フロー

Step 1
コインパック選択
->
Step 2
Stripe Checkout
->
Step 3
Webhook受信
->
Step 4
users/transactions 記録

B. ギフト送信フロー(トランザクション)

Step 1
ギフト選択
->
Step 2 (Transaction)
残高チェック(集計で確認)
+
Step 3 (Transaction)
streams/gifts に記録
+
Step 4 (Transaction)
users/giftHistory に記録
streams 本体への書き込みは不要。ギフト合計は COUNT(streams/gifts) × GIFT_DEFINITIONS[giftId].price で都度算出。

C. 収益分配

総売上
100%
->
Stripe手数料
3.6%
->
Gonmura
10%
->
事務所
~43.2%
+
JEENGROSS
~43.2%

D. パスコード認証フロー

Step 1
ファンがコード入力
->
Step 2
verifyPasscode CF
->
Step 3
agencies/streamers
.passcode と照合
->
一致
ライブ画面へ遷移

8. 導出ルール一覧

以下の値はDBに保存せず、API レスポンス生成時に都度算出する。
表示に必要な値 導出方法 使用画面
配信者名 users/{streamerId}.displayName ライブ画面、ホーム、管理画面
視聴者数(リアルタイム) COUNT(streams/{id}/viewers WHERE isActive=true) ライブ画面
ピーク視聴者数 MAX(streams/{id}/viewers の同時 isActive 数)
または配信終了時に1回だけ計算して streams に記録
配信統計
ギフト合計額 SUM(streams/{id}/gifts.totalCost) タレント画面、収益レポート
ギフト名・価格 GIFT_DEFINITIONS[giftId].name / .price ギフト履歴、管理画面
コメント投稿者名 users/{userId}.displayName ライブ画面コメント欄
投票数 COUNT(questions/{id}/votes) 質問投票箱
ビンゴ参加者数 COUNT(streams/{id}/bingoCards) ビンゴゲーム画面
ビンゴ勝者数 COUNT(bingo/current/winners) ビンゴゲーム画面
コイン残高 SUM(users/{id}/transactions.coins WHERE status=completed) - SUM(giftHistory → GIFT_DEFINITIONS[giftId].price) 全画面(ヘッダー表示等)
ユーザー累計消費 SUM(users/{id}/giftHistory → GIFT_DEFINITIONS[giftId].price) Gonmura管理画面ユーザー詳細
ユーザー累計ギフト数 COUNT(users/{id}/giftHistory) Gonmura管理画面ユーザー詳細
タレント数 COUNT(agencies/{id}/streamers WHERE status IN [active, inactive]) 事務所ダッシュボード
月次売上 SUM(streams WHERE agencyId=X AND startedAt in month → SUM(gifts価格)) 事務所ダッシュボード、収益画面
事務所名 agencies/{agencyId}.name Gonmura管理画面
タレント配信数 COUNT(streams WHERE streamerId==X) 事務所タレント一覧
タレント累計収益 SUM(streams WHERE streamerId=X → SUM(gifts価格)) × タレント分配率 事務所タレント一覧