Application de fitness intelligente : structure de données, schéma de base de données et spécifications de conception du serveur
ArtefactIl s'agit de l'artefact utilisé dans l'épisode 2 : Planifier les spécifications système de votre application (sans code). Vous pouvez utiliser cette spécification comme référence pour structurer les collections Cloud Firestore, les points de terminaison du serveur et la mémoire d'agent à trois niveaux.
Connectez-vous pour accéder à cet artefact
Connectez-vous avec votre profil de développeur Google pour afficher et copier l'artefact complet utilisé dans la série Build It!.
Profil de développeur requis
Vous n'êtes pas éligible à un profil de développeur. Demandez à votre administrateur Workspace d'activer cette fonctionnalité.
Créer un profil de développeur
Créez un profil de développeur Google sans frais pour débloquer tous les prompts et artefacts Build It!.
Rejoignez Builder Central pour débloquer
Récupérez votre badge de membre Builder sans frais en un clic pour débloquer tous les prompts et artefacts Build It!.
État : en cours (rédaction avec le compagnon de planification)
Base de données cible : Google Cloud Firestore
Objectif de l'architecture : reconstruction propre, facile à gérer et prête pour la production du prototype de fitness intelligent AI Studio.
0. Décisions concernant la pile technique de base et l'architecture du système
Pour garantir la stabilité, les performances et des limites claires en production, la référence architecturale suivante est verrouillée pour toutes les implémentations :
- Framework d'interface : Next.js (App Router, React 19) avec mise en cache des éléments hors connexion de la progressive web app (PWA) (à l'aide de Serwist/
@ducanh2912/next-pwa). - Composants de style et d'interface utilisateur : Tailwind CSS v4 + Tailwind Variants (pour la gestion des variantes) + Lucide React (icônes).
- Architecture de l'état du client :
- Zustand (avec le middleware
persist) : gère l'état du client local, tactile et éphémère (chronomètres, tampons d'ensembles actifs, graphique des pas et entrées non enregistrées de la salle de sport). - TanStack Query (React Query) : gère tous les états de serveur asynchrones, la mise en cache des documents Firestore, la revalidation en arrière-plan, la déduplication des requêtes et les mutations optimistes.
- Zustand (avec le middleware
- Environnement d'exécution backend et hébergement d'API : gestionnaires de routes Next.js (
app/api/...) hébergés sur Google Cloud Run dans la même région que Firestore et la mise en cache de contexte Gemini pour une latence interne inférieure à 15 ms. - Authentification : Firebase Authentication (Google Sign-In, adresse e-mail/mot de passe et association d'un processus d'intégration anonyme). Le client associe
Authorization: Bearer <Firebase_ID_Token>à toutes les requêtes API. Le serveur vérifie l'identité à l'aide defirebase-admin.auth().verifyIdToken(token). - Base de données principale : Google Cloud Firestore.
1. Présentation du système et parcours utilisateur principal
1.1 Principes de champ d'application et d'architecture
Cette spécification définit les modèles de données, les schémas Cloud Firestore, les contrats de point de terminaison de serveur et les structures de mémoire d'agent IA pour l'application Smart Fitness. Il établit une source unique de vérité pour la planification de tous les entraînements, l'historique des sessions et la mémoire de coaching autonome à plusieurs niveaux, complètement dissociée des frameworks de présentation de l'interface (qui sont traités dans une spécification de conception UI/UX dédiée).
1.2 Principes fondamentaux des données
- Séparation des responsabilités : frontière stricte entre les contrats de données (backend/base de données) et la conception de l'UI client.
- Architecture de bibliothèque hybride : encyclopédie d'exercices statique et immuable (
exercises) associée à une composition dynamique par IA des programmes hebdomadaires (weekly_plans). - Modifications par étapes avec intervention humaine : les modifications d'entraînement générées par l'IA sont renvoyées sous forme de propositions par étapes. Les écritures dans la base de données ne se produisent qu'après l'acceptation explicite de l'utilisateur.
- Mémoire vive continue à trois niveaux : l'IA capture, met à jour et purge de manière autonome les faits concernant l'utilisateur, qu'il s'agisse d'éléments permanents, d'états physiologiques temporaires (avec une durée de vie explicite) ou d'un contexte situationnel transitoire.
- Lectures de documents uniques précises : les données sont structurées dans des documents optimisés (comme des carnets à spirale pour les plans hebdomadaires et des buckets de chat hebdomadaires) afin de minimiser la latence de lecture et d'éliminer les requêtes redondantes.
2. Points de terminaison de serveur et contrats d'API
Chaque point de terminaison doit spécifier une source de réponse (
AGENT,DATABASEouHYBRID) afin que les points de terminaison basés sur un agent puissent être configurés de manière appropriée lors de l'implémentation.
POST /api/coach/chat
- Objectif : interaction conversationnelle avec le coach personnel IA, gestion des questions de coaching, soutien motivationnel et génération de propositions de modifications de l'entraînement (ajustements quotidiens et refontes hebdomadaires).
- Déclencheur : l'utilisateur envoie un message dans l'onglet "Coach" ou appuie sur un chip de suggestions contextuelles intégrées (par exemple, "Remplacer le développé Arnold par le développé épaules avec haltères").
- Source de la réponse :
AGENT(exploite Gemini avec des fonctionnalités d'appel de fonction/d'outil structuré et de streaming). Ne modifie pas directement la base de données. Renvoie une proposition préparée pour un examen avec intervention humaine. - En-têtes :
Authorization: Bearer <Firebase_ID_Token>,Content-Type: application/json
Charge utile de la requête :
{
"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)"
}
}Charge utile de la réponse :
{
"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)"
}
]
}- Double écriture dans
weekly_chats: à chaque tour, le serveur conserve le message utilisateur entrant (text) et la réponse du coach (text) dans/users/{userId}/weekly_chats/{week_start_date}à l'aide deFieldValue.arrayUnion(). - Modèle de mutation par étapes : renvoie la charge utile de la proposition sans modifier
/weekly_planstant que l'utilisateur n'appuie pas sur [Accepter le plan]. - Gestion autonome continue de la mémoire : émet
memory_mutationspour créer, mettre à jour ou supprimer des éléments du profil à trois niveaux de l'utilisateur.
POST /api/workouts/log
- Objectif : enregistre une séance d'entraînement terminée dans l'archive d'entraînement permanente de l'utilisateur, calcule les agrégats finaux de volume/répétitions, vérifie les records personnels et marque le jour planifié comme terminé.
- Déclencheur : l'utilisateur appuie sur "Terminer l'entraînement" sur l'écran de suivi de l'entraînement.
- Source de la réponse :
DATABASE– Transaction/écriture directe de la base de données dans Cloud Firestore.
Charge utile de la requête :
{
"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)"
}Charge utile de la réponse :
{
"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. Architecture de base de données Cloud Firestore
Collection : exercises
Hiérarchie : collection racine (/exercises/{exercise_id}) • Objectif : encyclopédie d'exercices statiques contenant des éléments multimédias, des indices de forme, des exigences en matière d'équipement et des instructions.
{
"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)"]
}Collection : weekly_plans
Hiérarchie : /users/{userId}/weekly_plans/{week_start_date} • Objectif : le "cahier à spirale" contenant le programme d'entraînement hebdomadaire complet, les métriques récapitulatives et les entraînements de chaque jour.
{
"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"
}
]
}
]
}
}
}Collection : weekly_chats
Hiérarchie : /users/{userId}/weekly_chats/{week_start_date} • Objectif : le "Weekly Chat Bucket" (bucket de chat hebdomadaire) contenant l'historique complet des conversations pour une semaine d'entraînement donnée dans une seule lecture de document.
{
"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)"
}
}
]
}Collection : workout_logs
Hiérarchie : /users/{userId}/workout_logs/{log_id} • Objectif : archive historique permanente et immuable des séances d'entraînement terminées.
{
"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)"
}Collection : user_profiles
Hiérarchie : /users/{userId}/profile/main • Objectif : "Tableau client dynamique" stockant le profil de mémoire à trois niveaux de l'utilisateur.
{
"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"
}
]
}Collection : prs
Hiérarchie : /users/{userId}/prs/{exercise_slug} • Objectif : stocke les records personnels de tous les temps, les repères de 1 RM estimés et les records spécifiques aux répétitions.
{
"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. Cycle de vie des données et pipeline d'agents
4.1 Pipeline 1 : coaching, génération d'entraînement et modification par étapes
- Action de l'utilisateur : envoie une saisie libre ou appuie sur un chip de modification contextuelle prégénéré.
- Assemblage du contexte : le serveur récupère le profil de mémoire actif à trois niveaux et le plan de la semaine en cours, en supprimant les éléments de mémoire expirés.
- Inférence de l'agent : Gemini traite la requête et le contexte à l'aide d'un appel d'outil/de fonction structuré.
- Mutation autonome de la mémoire : les faits extraits sur l'utilisateur sont appliqués à
/user_profiles/{userId}et synchronisés avecequipment_ids. - Staged Proposal Staging : les modifications d'entraînement proposées sont renvoyées avec
proposal.status = "pending"dans/weekly_chats/{week_start_date}. - Résolution et persistance de l'utilisateur : appuyer sur [Accept Plan] (Accepter le plan) valide les données proposées dans
/weekly_planset marque la proposition comme"accepted".
4.2 Pipeline 2 : exécution et archivage des entraînements (vérité terrain)
- Enregistrement de la session en direct : le client suit la charge réelle soulevée, les répétitions effectuées et les indicateurs de fin dans l'état de l'UI locale.
- Envoi de la session : le client appelle
POST /api/workouts/logsur "Terminer l'entraînement". - Calcul et validation des agrégats : le serveur calcule le volume cumulé de la session et vérifie les records personnels à l'aide de la formule de Brzycki.
- Persistance immuable : écrit un enregistrement immuable dans
/workout_logs, marque le jour"completed"dans/weekly_planset effectue un upsert des nouvelles demandes d'extraction dans/prs.
4.3 Pipeline 3 : ingestion et invalidation autonomes de la mémoire
- Ingestion : les faits sont extraits en continu de toutes les interactions conversationnelles.
- Classification : niveau 1 (produits de base, sans expiration), niveau 2 (état physiologique, TTL de 3 à 4 jours), niveau 3 (contexte situationnel, TTL basé sur l'événement).
- Élagage et invalidation : les éléments arrivés à expiration (
current_timestamp > expires_at) sont filtrés du contexte de la requête et purgés de manière différée.
5. Règles de sécurité Firestore et spécifications d'index composites
5.1 Règles de sécurité (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 Index de requêtes composites (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. Journal des modifications et décisions verrouillées
| Date | Composant/Décision | Type | Verrouillé par |
|---|---|---|---|
| Configuration initiale | Échafaudage de spécification vide initialisé | Installation du système | Équipe |
| 2026-09-04 | Ajout des collections Firestore : exercises et weekly_plans | Schéma | Équipe |
| 2026-09-04 | Ajout du point de terminaison POST /api/coach/chat et de la collection chat_messages | Point de terminaison et schéma | Équipe |
| 2026-09-04 | Refactorisation du stockage des discussions vers le schéma de bucket weekly_chats | Schéma | Équipe |
| 2026-09-04 | Ajout du point de terminaison POST /api/workouts/log et de la collection workout_logs | Point de terminaison et schéma | Équipe |
| 2026-09-04 | Ajout du schéma de mémoire à trois niveaux user_profiles et mise à jour de POST /api/coach/chat avec memory_mutations | Point de terminaison et schéma | Équipe |
| 2026-09-04 | Avoir terminé la section 1 (Champ d'application et principes du système) et la section 4 (Cycle de vie et pipelines des données) | Saisie automatique des spécifications complètes | Équipe |
| 2026-09-22 | Spécifications harmonisées : pile Next.js + Tailwind + Zustand, authentification Bearer, sous-collection de RP, règles Firestore et slugs d'exercice canoniques | Harmonisation complète des spécifications | Examen de l'architecture |