スマート フィットネス アプリ: データ構造、データベース スキーマ、サーバー設計仕様
アーティファクトこれは、エピソード 2「アプリのシステム仕様を計画する(ノーコード)」で使用されるアーティファクトです。この仕様は、Cloud Firestore コレクション、サーバー エンドポイント、3 層エージェント メモリを構造化する際のリファレンスとして使用できます。
このアーティファクトにアクセスするにはログインしてください
Google デベロッパー プロフィールでログインすると、Build It! シリーズで使用されているアーティファクト全体を表示してコピーできます。
デベロッパー プロフィールが必要です
デベロッパー プロフィールは無効です。この機能を有効にするよう、Workspace 管理者に連絡してください。
デベロッパー プロフィールを作成する
無料の Google デベロッパー プロフィールを作成して、Build It! のすべてのプロンプトとアーティファクトを利用できるようにしましょう。
Builder Central に参加してロックを解除
ワンクリックで無料の Builder メンバーシップ バッジを取得して、Build It! のすべてのプロンプトとアーティファクトにアクセスしましょう。
ステータス: 進行中(Planning Companion でドラフト作成中)
ターゲット データベース: Google Cloud Firestore
アーキテクチャの目標: AI Studio スマート フィットネス プロトタイプを、クリーンでメンテナンスしやすく、本番環境に対応した形で再構築する。
0: コア技術スタックとシステム アーキテクチャの決定
本番環境の安定性、パフォーマンス、明確な境界を確保するため、すべての実装で次のアーキテクチャ ベースラインがロックされています。
- フロントエンド フレームワーク: Next.js(App Router、React 19)。プログレッシブ ウェブアプリ(PWA)のオフライン アセット キャッシュ(Serwist /
@ducanh2912/next-pwaを使用)を使用。 - スタイル設定と UI コンポーネント: Tailwind CSS v4 + Tailwind Variants(バリアント管理用)+ Lucide React(アイコン)。
- クライアントの状態のアーキテクチャ:
- Zustand(
persistミドルウェアを使用): ローカル、触覚、エフェメラルのクライアント状態(ストップウォッチ タイマー、アクティブ セット バッファ、ステップグラフ、未保存のジムフロア入力)を処理します。 - TanStack Query(React Query): すべての非同期サーバー状態、Firestore ドキュメントのキャッシュ、バックグラウンドでの再検証、リクエストの重複除去、楽観的なミューテーションを処理します。
- Zustand(
- バックエンド ランタイムと API ホスティング: Firestore と Gemini コンテキスト キャッシュと同じリージョンにある Google Cloud Run でホストされる Next.js ルート ハンドラ(
app/api/...)。内部レイテンシは 15 ミリ秒未満です。 - 認証: Firebase Authentication(Google ログイン、メール / パスワード、匿名オンボーディング リンク)。クライアントはすべての API リクエストに
Authorization: Bearer <Firebase_ID_Token>を添付し、サーバーはfirebase-admin.auth().verifyIdToken(token)を使用して ID を検証します。 - プライマリ データベース: Google Cloud Firestore。
1. システムの概要とコア ユーザー ジャーニー
1.1 範囲とアーキテクチャの原則
この仕様では、Smart Fitness アプリケーションのデータモデル、Cloud Firestore スキーマ、サーバー エンドポイント コントラクト、AI エージェント メモリ構造を定義します。すべてのワークアウト プランニング、過去のセッション ログ、自律的な階層型コーチング メモリの信頼できる唯一の情報源を確立し、フロントエンドのプレゼンテーション フレームワーク(専用の UI / UX 設計仕様で対応)から完全に切り離します。
1.2 データに関する基本原則
- 関心の分離: データ契約(バックエンド / データベース)とクライアント UI 設計の間の厳格な境界。
- ハイブリッド ライブラリ アーキテクチャ: 静的で不変のエクササイズ百科事典(
exercises)と、週単位のスケジュールの動的な AI 構成(weekly_plans)を組み合わせたもの。 - 人間参加型のステージングされたミューテーション: AI が生成したワークアウトの変更は、ステージングされた提案として返されます。データベースへの書き込みは、ユーザーが明示的に承認した場合にのみ行われます。
- 継続的な 3 層の生きたメモリ: AI は、永続的な基本情報、期限切れの生理学的状態(明示的な TTL を含む)、一時的な状況コンテキストにわたって、ユーザーの事実を自律的にキャプチャ、更新、パージします。
- きめ細かい単一ドキュメントの読み取り: データは、読み取りレイテンシを最小限に抑え、冗長なクエリを排除するために、最適化されたドキュメント(週次計画のスパイラル ノートブックや週次チャット バケットなど)に構造化されます。
2. サーバー エンドポイントと API 契約
エージェントがサポートするエンドポイントを実装時に適切に構成できるように、すべてのエンドポイントでレスポンス ソース(
AGENT、DATABASE、HYBRID)を指定する必要があります。
POST /api/coach/chat
- 目的: AI パーソナル トレーナーとの会話型インタラクション。コーチングに関する質問への対応、モチベーションのサポート、ワークアウトの変更案(日々の調整と週単位のオーバーホールの両方)の生成。
- トリガー: ユーザーが [コーチ] タブでメッセージを送信するか、インラインのコンテキストに応じた編集候補チップ(例: 「アーノルド プレスをダンベル ショルダー プレスに置き換える」)をタップします。
- レスポンス ソース:
AGENT- 構造化されたツール / 関数呼び出しとストリーミング機能を備えた Gemini を活用します。データベースを直接変更せず、人間参加型のレビュー用にステージングされた提案を返します。 - ヘッダー:
Authorization: Bearer <Firebase_ID_Token>、Content-Type: application/json
リクエスト ペイロード:
{
"message": "string (user input or selected prompt chip text)",
"context": {
"active_tab": "string (enum: today | week | coach)",
"week_start_date": "string (ISO date YYYY-MM-DD)",
"selected_day": "string (e.g. monday, optional)"
}
}レスポンス ペイロード:
{
"message": "string (conversational coaching response displayed in chat bubble)",
"proposal": {
"id": "string (unique proposal ID, nullable)",
"type": "string (enum: NONE | UPDATE_DAY | UPDATE_WEEK)",
"target_day": "string (e.g. monday, nullable)",
"summary_diff": "string (concise summary of changes, e.g. 'Swapped Arnold Press ➔ DB Shoulder Press', nullable)",
"data": "map (full proposed workout day object or weekly plan object, nullable)"
},
"memory_mutations": [
{
"operation": "string (enum: CREATE | UPDATE | DELETE)",
"target_tier": "string (enum: tier_1_staples | tier_2_physiological | tier_3_situational)",
"memory_id": "string (required for UPDATE and DELETE)",
"entry": "map (memory payload with fact, category, expires_at, etc., required for CREATE and UPDATE)",
"reason": "string (concise rationale for why memory was mutated)"
}
]
}weekly_chatsへのデュアル書き込み: 各ターンで、サーバーはFieldValue.arrayUnion()を使用して、受信したユーザー メッセージ(text)とコーチのレスポンス(text)を/users/{userId}/weekly_chats/{week_start_date}に永続化します。- 段階的ミューテーション パターン: ユーザーが [プランを承認] をタップするまで、
/weekly_plansを変更せずに提案ペイロードを返します。 - 継続的な自律型メモリ管理:
memory_mutationsを出力して、ユーザーの 3 層プロファイルからアイテムを作成、更新、削除します。
POST /api/workouts/log
- 目的: 完了したワークアウト セッションをユーザーの永続的なトレーニング アーカイブに保存し、最終的なボリューム / レップ数の集計を計算し、個人記録(PR)を確認し、スケジュールされた日を完了としてマークします。
- トリガー: ユーザーがワークアウトの記録画面で [ワークアウトを終了] をタップします。
- レスポンス ソース:
DATABASE- Cloud Firestore への直接のデータベース トランザクション / 書き込み。
リクエスト ペイロード:
{
"plan_reference": {
"week_start_date": "string (ISO date YYYY-MM-DD)",
"day_of_week": "string (e.g. monday)"
},
"title": "string (e.g. Upper Body Hypertrophy)",
"started_at": "string (ISO timestamp)",
"completed_at": "string (ISO timestamp)",
"duration_seconds": "number (e.g. 3120)",
"exercises_logged": [
{
"exercise_id": "string (canonical slug, e.g. incline-db-bench-press)",
"exercise_name": "string (e.g. Incline DB Bench Press)",
"sets": [
{
"set_number": "number (e.g. 1)",
"weight_lbs": "number (e.g. 60)",
"reps_completed": "number (e.g. 12)",
"completed": "boolean"
}
]
}
],
"user_notes": "string (optional subjective feedback)",
"perceived_exertion": "number (optional 1-10 RPE rating)"
}レスポンス ペイロード:
{
"success": "boolean",
"log_id": "string (newly created workout log document ID)",
"summary": {
"total_volume_lbs": "number",
"total_sets_completed": "number",
"total_reps_completed": "number"
},
"new_prs": [
{
"exercise_id": "string (canonical slug, e.g. incline-db-bench-press)",
"metric": "string (e.g. '10 Rep Max' or 'Estimated 1RM')",
"value": "string (e.g. '65 lbs' or '105 lbs')"
}
]
}3. Cloud Firestore データベースのアーキテクチャ
コレクション: exercises
階層: ルート コレクション(/exercises/{exercise_id})• 目的: マルチメディア、フォームのヒント、必要な器具、手順を含む、静的なエクササイズ百科事典。
{
"id": "string (canonical slug matching document ID, e.g. incline-db-bench-press)",
"name": "string (e.g. Incline DB Bench Press)",
"category": "string (e.g. Strength, Hypertrophy, Cardio, Mobility)",
"target_muscles": {
"primary": ["string (e.g. Upper Chest)"],
"secondary": ["string (e.g. Front Deltoids, Triceps)"]
},
"media": {
"video_url": "string (URL to demonstration video)",
"poster_url": "string (URL to video poster / thumbnail image)"
},
"instructions": ["string (step-by-step execution guidance)"],
"form_tips": ["string (critical cues and common pitfalls)"],
"equipment_required": ["string (e.g. Dumbbells, Incline Bench)"]
}コレクション: weekly_plans
階層: /users/{userId}/weekly_plans/{week_start_date} • 目的: 1 週間のトレーニング スケジュール全体、指標の概要、個々の日のワークアウトを含む「スパイラル ノートブック」。
{
"week_start_date": "string (ISO date YYYY-MM-DD, e.g. 2026-09-07)",
"theme": "string (e.g. Hypertrophy Focus & Upper Body Overload)",
"summary": {
"total_time_minutes": "number (raw minutes, e.g. 180)",
"total_volume_lbs": "number (raw tonnage in lbs, e.g. 38400)",
"frequency": "number (active workout days, e.g. 4)"
},
"suggested_edits": ["string (pre-generated AI prompt suggestion)"],
"days": {
"monday": {
"date": "string (ISO date YYYY-MM-DD)",
"day_of_week": "string (e.g. Monday)",
"is_rest_day": "boolean",
"status": "string (enum: scheduled | completed | skipped)",
"completed_log_id": "string (nullable)",
"completed_at": "string (ISO timestamp, nullable)",
"title": "string (e.g. Upper Body Hypertrophy)",
"focus_tag": "string (e.g. Push & Chest Dominant)",
"duration_minutes": "number",
"estimated_volume_lbs": "number",
"coach_note": "string",
"muscle_focus": [{ "muscle": "string", "intensity": "number (0-100)" }],
"suggested_edits": ["string"],
"blocks": [
{
"label": "string (e.g. Main Compound Block)",
"exercises": [
{
"exercise_id": "string",
"name": "string",
"target_sets": "number",
"target_reps": "string or number",
"rest_seconds": "number"
}
]
}
]
}
}
}コレクション: weekly_chats
階層: /users/{userId}/weekly_chats/{week_start_date} • 目的: 特定のトレーニング週の完全な会話履歴を 1 つのドキュメント読み取りに含む「週次チャット バケット」。
{
"week_start_date": "string (ISO date YYYY-MM-DD, e.g. 2026-09-07)",
"messages": [
{
"id": "string (unique message ID)",
"sender": "string (enum: user | coach)",
"text": "string (conversational message body)",
"created_at": "timestamp (server timestamp)",
"proposal": {
"id": "string (proposal UID, nullable)",
"type": "string (enum: UPDATE_DAY | UPDATE_WEEK, nullable)",
"target_day": "string (e.g. monday, nullable)",
"summary_diff": "string (nullable)",
"status": "string (enum: pending | accepted | dismissed)",
"data": "map (workout or week snapshot payload, nullable)"
}
}
]
}コレクション: workout_logs
階層: /users/{userId}/workout_logs/{log_id} • 目的: 完了したワークアウト セッションの永続的で不変の履歴アーカイブ。
{
"id": "string (document ID)",
"plan_reference": {
"week_start_date": "string (ISO date YYYY-MM-DD)",
"day_of_week": "string (e.g. monday)"
},
"title": "string (e.g. Upper Body Hypertrophy)",
"started_at": "timestamp",
"completed_at": "timestamp",
"duration_seconds": "number",
"summary": {
"total_volume_lbs": "number",
"total_sets_completed": "number",
"total_reps_completed": "number"
},
"exercises_logged": [
{
"exercise_id": "string",
"exercise_name": "string",
"sets": [
{
"set_number": "number",
"weight_lbs": "number",
"reps_completed": "number",
"completed": "boolean"
}
]
}
],
"user_notes": "string (nullable)",
"perceived_exertion": "number (1-10 RPE, nullable)"
}コレクション: user_profiles
階層: /users/{userId}/profile/main • 目的: ユーザーの 3 階層のメモリ プロファイルを保存する「Living Client Chart」。
{
"user_id": "string (document owner UID)",
"display_name": "string (e.g. Alex)",
"email": "string (e.g. alex@example.com)",
"preferences": {
"units": "string (enum: lbs | kg)",
"haptics": "boolean",
"sound": "boolean"
},
"equipment_ids": ["string (e.g. 'dumbbells', 'bench_adjustable', 'pull_up_bar')"],
"onboarding_status": "string (enum: not_started | in_progress | completed)",
"created_at": "timestamp",
"tier_1_staples": [
{
"id": "string (e.g. mem_01)",
"category": "string (enum: equipment | preference | goal | chronic_condition)",
"fact": "string (e.g. Home gym with dumbbells up to 50 lbs and adjustable bench)",
"source": "string (e.g. conversational_onboarding | manual_entry)"
}
],
"tier_2_physiological": [
{
"id": "string (e.g. mem_02)",
"body_part": "string (e.g. hamstrings, shoulder)",
"condition": "string (e.g. soreness, acute_fatigue, minor_strain)",
"fact": "string (e.g. Hamstrings are moderately sore from hill sprints)",
"effective_date": "string (ISO date YYYY-MM-DD)",
"expires_at": "timestamp"
}
],
"tier_3_situational": [
{
"id": "string (e.g. mem_03)",
"event": "string (e.g. golf_competition, travel)",
"fact": "string (e.g. Major golf tournament on Saturday; avoid excessive arm/lat fatigue on Friday)",
"week_start_date": "string (ISO date YYYY-MM-DD)",
"expires_at": "timestamp"
}
]
}コレクション: prs
階層: /users/{userId}/prs/{exercise_slug} • 目的: 過去すべての個人記録、推定 1 回最大反復回数ベンチマーク、反復回数別の記録を保存します。
{
"exercise_id": "string (canonical slug, e.g. incline-db-bench-press)",
"exercise_name": "string (e.g. Incline DB Bench Press)",
"estimated_1rm": "number (calculated using Brzycki formula, e.g. 105)",
"max_weight_set": {
"weight_lbs": "number (e.g. 80)",
"reps": "number (e.g. 10)",
"achieved_at": "timestamp"
},
"rep_records": {
"10": 80,
"8": 85,
"5": 95
},
"last_updated": "timestamp"
}4. データ ライフサイクルとエージェント パイプライン
4.1 パイプライン 1: コーチング、ワークアウトの生成、段階的な変更
- ユーザーの操作: 自由形式の入力を送信するか、事前に生成されたコンテキスト編集チップをタップします。
- コンテキストの組み立て: サーバーがアクティブな 3 層メモリ プロファイルと今週のプランを取得し、期限切れのメモリ項目を削除します。
- エージェント推論: Gemini は、構造化されたツール / 関数呼び出しを使用してプロンプトとコンテキストを処理します。
- 自律的なメモリ変更: 抽出されたユーザーの事実は
/user_profiles/{userId}に適用され、equipment_idsと同期されます。 - Staged Proposal Staging: 提案されたワークアウトの変更は、
/weekly_chats/{week_start_date}内のproposal.status = "pending"で返されます。 - ユーザーの解決と永続性: [プランを承認] をタップすると、提案されたデータが
/weekly_plansに commit され、提案に"accepted"のマークが付けられます。
4.2 パイプライン 2: ワークアウトの実行とアーカイブ(グラウンド トゥルース)
- ライブ セッションの記録: クライアントは、実際に持ち上げた負荷、完了した繰り返し回数、完了フラグをローカル UI 状態に記録します。
- セッションの送信: クライアントが [Finish Workout] で
POST /api/workouts/logを呼び出します。 - 集計計算と検証: サーバーが累積セッション量を計算し、Brzycki の公式を使用して PR をチェックします。
- 不変の永続性:
/workout_logsに不変のレコードを書き込み、/weekly_plansの"completed"日をマークし、新しい PR を/prsにアップサートします。
4.3 パイプライン 3: 自律的なメモリの取り込みと無効化
- 取り込み: すべての会話型インタラクションからファクトが継続的に抽出されます。
- 分類: Tier 1(基本、有効期限なし)、Tier 2(生理学的状態、3 ~ 4 日の TTL)、Tier 3(状況コンテキスト、イベントベースの TTL)。
- プルーニングと無効化: 有効期限が切れたアイテム(
current_timestamp > expires_at)は、プロンプト コンテキストから除外され、遅延パージされます。
5. Firestore セキュリティ ルールと複合インデックスの仕様
5.1 セキュリティ ルール(firestore.rules)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
match /profile/{document=**} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /weekly_plans/{planId} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /weekly_chats/{chatId} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /prs/{exerciseSlug} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /workout_logs/{logId} {
allow read, create: if request.auth != null && request.auth.uid == userId;
allow update, delete: if false;
}
}
match /exercises/{exerciseId} {
allow read: if request.auth != null;
allow write: if false;
}
}
}5.2 複合クエリ インデックス(firestore.indexes.json)
{
"indexes": [
{
"collectionGroup": "workout_logs",
"queryScope": "COLLECTION",
"fields": [{ "fieldPath": "completed_at", "order": "DESCENDING" }]
},
{
"collectionGroup": "weekly_plans",
"queryScope": "COLLECTION",
"fields": [{ "fieldPath": "week_start_date", "order": "DESCENDING" }]
},
{
"collectionGroup": "prs",
"queryScope": "COLLECTION",
"fields": [{ "fieldPath": "last_updated", "order": "DESCENDING" }]
}
]
}6. 変更履歴とロックされた決定
| 日付 | コンポーネント / 決定 | タイプ | ロックしたユーザー |
|---|---|---|---|
| 初期設定 | 空の仕様スキャフォールドを初期化しました | システム設定 | チーム |
| 2026-09-04 | Firestore コレクション(exercises と weekly_plans)を追加しました | スキーマ | チーム |
| 2026-09-04 | POST /api/coach/chat エンドポイントと chat_messages コレクションを追加しました | エンドポイントとスキーマ | チーム |
| 2026-09-04 | チャット ストレージを weekly_chats バケット スキーマにリファクタリング | スキーマ | チーム |
| 2026-09-04 | POST /api/workouts/log エンドポイントと workout_logs コレクションを追加しました | エンドポイントとスキーマ | チーム |
| 2026-09-04 | user_profiles の 3 階層メモリ スキーマを追加し、POST /api/coach/chat を memory_mutations で更新しました | エンドポイントとスキーマ | チーム |
| 2026-09-04 | セクション 1(システムのスコープと原則)とセクション 4(データ ライフサイクルとパイプライン)を完了 | 仕様の完全な完成 | チーム |
| 2026-09-22 | 統一された仕様: Next.js + Tailwind + Zustand スタック、Bearer 認証、PR サブコレクション、Firestore ルール、正規のエクササイズ スラッグ | 仕様の完全なハーモナイゼーション | アーキテクチャ レビュー |