Smart Fitness App: Data Structure, Database Schema & Server Design Spec
ArtefattoQuesto è l'artefatto utilizzato nell'episodio 2: Pianifica le specifiche di sistema della tua app (nessun codice). Puoi utilizzare questa specifica come riferimento per strutturare le raccolte Cloud Firestore, gli endpoint server e la memoria dell'agente a 3 livelli.
Accedi per visualizzare questo artefatto
Accedi con il tuo Profilo sviluppatore Google per visualizzare e copiare l'artefatto completo utilizzato nella serie Build It!.
Profilo sviluppatore richiesto
Non hai l'idoneità per un profilo sviluppatore. Chiedi all'amministratore di Workspace di attivare questa funzionalità.
Crea un Profilo sviluppatore
Crea un profilo sviluppatore Google senza costi per sbloccare tutti i prompt e gli artefatti di Build It!
Iscriviti a Builder Central per sbloccare
Richiedi il tuo badge per gli abbonati Builder senza costi con un solo clic per sbloccare tutti i prompt e gli artefatti di Build It!.
Stato: in corso (bozza con Planning Companion)
Database di destinazione: Google Cloud Firestore
Obiettivo dell'architettura: ricostruzione pulita, gestibile e pronta per la produzione del prototipo di smart fitness di AI Studio.
0. Stack tecnico di base e decisioni sull'architettura del sistema
Per garantire la stabilità della produzione, le prestazioni e limiti chiari, la seguente base di riferimento dell'architettura è bloccata per tutte le implementazioni:
- Framework frontend: Next.js (App Router, React 19) con memorizzazione nella cache degli asset offline delle app web progressive (PWA) (utilizzando Serwist/
@ducanh2912/next-pwa). - Stile e componenti UI: Tailwind CSS v4 + Tailwind Variants (per la gestione delle varianti) + Lucide React (icone).
- Architettura dello stato del client:
- Zustand (con middleware
persist): gestisce lo stato del client locale, tattile ed effimero (timer del cronometro, buffer dei set attivi, grafico dei passi e input non salvati della palestra). - TanStack Query (React Query): gestisce tutto lo stato del server asincrono, la memorizzazione nella cache dei documenti Firestore, la riconvalida in background, la deduplicazione delle richieste e le mutazioni ottimistiche.
- Zustand (con middleware
- Runtime di backend e hosting di API: gestori di route Next.js (
app/api/...) ospitati su Google Cloud Run nella stessa regione di Firestore e memorizzazione nella cache del contesto di Gemini per una latenza interna inferiore a 15 ms. - Autenticazione: Firebase Authentication (Accedi con Google, email/password e collegamento anonimo all'onboarding). Il client allega
Authorization: Bearer <Firebase_ID_Token>a tutte le richieste API; il server verifica l'identità utilizzandofirebase-admin.auth().verifyIdToken(token). - Database primario: Google Cloud Firestore.
1. Panoramica del sistema e percorso dell'utente principale
1.1 Ambito e principi di architettura
Questa specifica definisce i modelli di dati, gli schemi Cloud Firestore, i contratti degli endpoint server e le strutture di memoria degli agenti AI per l'applicazione Smart Fitness. Stabilisce una Single Source Of Truth per tutta la pianificazione degli allenamenti, la registrazione delle sessioni storiche e la memoria di coaching autonomo a più livelli, completamente disaccoppiata dai framework di presentazione frontend (che sono trattati in una specifica di progettazione UI/UX dedicata).
1.2 Principi fondamentali dei dati
- Separazione delle responsabilità: confine netto tra i contratti di dati (backend/database) e il design della UI del client.
- Architettura della libreria ibrida: enciclopedia degli esercizi statica e immutabile (
exercises) unita alla composizione dinamica basata sull'AI dei programmi settimanali (weekly_plans). - Mutazioni in fasi con intervento umano: le modifiche all'allenamento generate dall'AI vengono restituite come proposte in fasi; le scritture nel database avvengono esclusivamente previa accettazione esplicita dell'utente.
- Memoria attiva continua a 3 livelli: l'AI acquisisce, aggiorna ed elimina autonomamente i dati dell'utente in base a elementi permanenti, stati fisiologici in scadenza (con TTL esplicito) e contesto situazionale transitorio.
- Letture granulari di singoli documenti: i dati sono strutturati in documenti ottimizzati (come i quaderni a spirale del piano settimanale e i bucket di chat settimanali) per ridurre al minimo la latenza di lettura ed eliminare le query ridondanti.
2. Endpoint server e contratti API
Ogni endpoint deve specificare l'origine della risposta (
AGENT,DATABASEoHYBRID) in modo che gli endpoint supportati dall'agente possano essere configurati in modo appropriato durante l'implementazione.
POST /api/coach/chat
- Scopo: interazione conversazionale con il personal trainer AI, gestione delle domande di coaching, supporto motivazionale e generazione di modifiche proposte per l'allenamento (sia modifiche giornaliere che revisioni settimanali).
- Attivatore: l'utente invia un messaggio nella scheda Coach o tocca un chip di suggerimenti contestuali in linea (ad es. "Sostituisci Arnold press con DB shoulder press").
- Origine della risposta:
AGENT- Sfrutta Gemini con funzionalità di chiamata di funzione/strumento strutturate e di streaming. Non modifica direttamente il database, ma restituisce una proposta in fase di preparazione per la revisione human-in-the-loop. - Intestazioni:
Authorization: Bearer <Firebase_ID_Token>,Content-Type: application/json
Payload della richiesta:
{
"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)"
}
}Payload della risposta:
{
"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)"
}
]
}- Doppia scrittura in
weekly_chats: a ogni turno, il server conserva il messaggio dell'utente in arrivo (text) e la risposta del coach (text) in/users/{userId}/weekly_chats/{week_start_date}utilizzandoFieldValue.arrayUnion(). - Pattern di mutazione in fasi: restituisce il payload della proposta senza modificare
/weekly_plansfinché l'utente non tocca [Accetta piano]. - Gestione della memoria autonoma continua: genera
memory_mutationsper creare, aggiornare o eliminare elementi dal profilo a 3 livelli dell'utente.
POST /api/workouts/log
- Scopo: salva una sessione di allenamento completata nell'archivio di allenamento permanente dell'utente, calcola gli aggregati finali di volume/ripetizioni, controlla i record personali (PR) e contrassegna il giorno programmato come completato.
- Trigger: l'utente tocca "Termina allenamento" nella schermata di monitoraggio dell'allenamento.
- Origine della risposta:
DATABASE- Transazione/scrittura diretta nel database in Cloud Firestore.
Payload della richiesta:
{
"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)"
}Payload della risposta:
{
"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. Architettura del database Cloud Firestore
Raccolta: exercises
Gerarchia: raccolta principale (/exercises/{exercise_id}) • Scopo: enciclopedia statica degli esercizi contenente contenuti multimediali, indicazioni sulla forma, requisiti delle attrezzature e istruzioni.
{
"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)"]
}Raccolta: weekly_plans
Gerarchia: /users/{userId}/weekly_plans/{week_start_date} • Scopo: il "quaderno a spirale" contenente il programma di allenamento settimanale completo, le metriche di riepilogo e gli allenamenti dei singoli giorni.
{
"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"
}
]
}
]
}
}
}Raccolta: weekly_chats
Gerarchia: /users/{userId}/weekly_chats/{week_start_date} • Scopo: il "Weekly Chat Bucket" contiene la cronologia completa delle conversazioni per una determinata settimana di addestramento in un'unica lettura del documento.
{
"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)"
}
}
]
}Raccolta: workout_logs
Gerarchia: /users/{userId}/workout_logs/{log_id} • Scopo: archivio storico permanente e immutabile delle sessioni di allenamento completate.
{
"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)"
}Raccolta: user_profiles
Gerarchia: /users/{userId}/profile/main • Scopo: il "Living Client Chart" che memorizza il profilo di memoria a 3 livelli dell'utente.
{
"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"
}
]
}Raccolta: prs
Gerarchia: /users/{userId}/prs/{exercise_slug} • Scopo: memorizza i record personali di tutti i tempi, i benchmark stimati di 1 ripetizione massima e i record specifici delle ripetizioni.
{
"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. Ciclo di vita dei dati e pipeline degli agenti
4.1 Pipeline 1: Coaching, Workout Generation & Staged Modification
- Azione dell'utente: invia un input in formato libero o tocca un chip di modifica contestuale pregenerato.
- Assemblaggio del contesto: il server recupera il profilo di memoria attivo a 3 livelli e il piano della settimana corrente, eliminando gli elementi di memoria scaduti.
- Inferenza dell'agente: Gemini elabora il prompt e il contesto utilizzando la chiamata di funzione/strumento strutturata.
- Mutazione autonoma della memoria: i dati utente estratti vengono applicati a
/user_profiles/{userId}e sincronizzati conequipment_ids. - Staged Proposal Staging: le modifiche proposte all'allenamento vengono restituite con
proposal.status = "pending"all'interno di/weekly_chats/{week_start_date}. - Risoluzione e persistenza dell'utente: toccando [Accept Plan], i dati proposti vengono inviati a
/weekly_planse la proposta viene contrassegnata come"accepted".
4.2 Pipeline 2: esecuzione e archiviazione degli allenamenti (dato di fatto)
- Registrazione della sessione in tempo reale: il client monitora il carico effettivo sollevato, le ripetizioni completate e i flag di completamento nello stato dell'UI locale.
- Invio della sessione: il client chiama
POST /api/workouts/logsu "Termina allenamento". - Calcolo e convalida degli aggregati: il server calcola il volume cumulativo della sessione e verifica la presenza di record personali utilizzando la formula di Brzycki.
- Persistenza immutabile: scrive un record immutabile in
/workout_logs, contrassegna il giorno"completed"in/weekly_planse applica l'upsert alle nuove PR in/prs.
4.3 Pipeline 3: Autonomous Memory Ingestion & Invalidation
- Importazione: i fatti vengono estratti continuamente da tutte le interazioni conversazionali.
- Classificazione: livello 1 (elementi di base, nessuna scadenza), livello 2 (stato fisiologico, TTL di 3-4 giorni), livello 3 (contesto situazionale, TTL basato sugli eventi).
- Eliminazione e annullamento della validità: gli elementi scaduti (
current_timestamp > expires_at) vengono filtrati dal contesto del prompt ed eliminati in modo lazy.
5. Regole di sicurezza di Firestore e specifiche degli indici composti
5.1 Regole di sicurezza (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 Indici di query composte (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. Log delle modifiche e decisioni bloccate
| Data | Componente/Decisione | Tipo | Bloccato da |
|---|---|---|---|
| Configurazione iniziale | Initialized empty specification scaffold | Configurazione del sistema | Team |
| 2026-09-04 | Aggiunta di raccolte Firestore: exercises e weekly_plans | Schema | Team |
| 2026-09-04 | Aggiunto l'endpoint POST /api/coach/chat e la raccolta chat_messages | Endpoint e schema | Team |
| 2026-09-04 | Rifattorizzazione dello spazio di archiviazione della chat nello schema del bucket weekly_chats | Schema | Team |
| 2026-09-04 | Aggiunto l'endpoint POST /api/workouts/log e la raccolta workout_logs | Endpoint e schema | Team |
| 2026-09-04 | Aggiunto lo schema di memoria a 3 livelli user_profiles e aggiornato POST /api/coach/chat con memory_mutations | Endpoint e schema | Team |
| 2026-09-04 | Completamento della Sezione 1 (Ambito e principi del sistema) e della Sezione 4 (Ciclo di vita e pipeline dei dati) | Completamento delle specifiche complete | Team |
| 2026-09-22 | Specifiche armonizzate: stack Next.js + Tailwind + Zustand, autenticazione Bearer, sottoraccolta PR, regole di Firestore e slug di esercizi canonici | Armonizzazione completa delle specifiche | Revisione dell'architettura |