App de fitness inteligente: estructura de datos, esquema de la base de datos y especificación de diseño del servidor
ArtefactoEste es el artefacto que se usa en el episodio 2: Planifica las especificaciones del sistema de tu app (sin código). Puedes usar esta especificación como referencia para estructurar colecciones de Cloud Firestore, extremos de servidor y memoria de agentes de 3 niveles.
Accede para ver este artefacto
Accede con tu Perfil del desarrollador de Google para ver y copiar el artefacto completo que se usa en la serie Build It!
Se requiere un perfil de desarrollador
No cumples con los requisitos para tener un perfil del desarrollador. Notifica a tu administrador de Workspace para que habilite esta función.
Crear un Perfil del desarrollador
Crea un Perfil del desarrollador de Google gratuito para desbloquear todas las instrucciones y los artefactos de Build It!
Únete a Builder Central para desbloquear
Reclama tu insignia de miembro de Builder sin costo con un solo clic para desbloquear todas las instrucciones y los artefactos de Build It.
Estado: En curso (redacción con Planning Companion)
Base de datos de destino: Google Cloud Firestore
Objetivo de la arquitectura: Reconstrucción limpia, mantenible y lista para producción del prototipo de fitness inteligente de AI Studio.
0. Decisiones sobre la pila técnica principal y la arquitectura del sistema
Para garantizar la estabilidad de la producción, el rendimiento y los límites claros, se bloquea la siguiente base de arquitectura para todas las implementaciones:
- Framework de frontend: Next.js (App Router, React 19) con almacenamiento en caché de recursos sin conexión de app web progresiva (PWA) (con Serwist/
@ducanh2912/next-pwa). - Estilo y componentes de la IU: Tailwind CSS v4 + Tailwind Variants (para la administración de variantes) + Lucide React (íconos).
- Arquitectura de estado del cliente:
- Zustand (con middleware de
persist): Controla el estado del cliente local, táctil y efímero (cronómetros, búferes de series activas, gráfico de pasos y entradas de gimnasio no guardadas). - TanStack Query (React Query): Controla todo el estado asíncrono del servidor, el almacenamiento en caché de documentos de Firestore, la revalidación en segundo plano, la anulación de duplicación de solicitudes y las mutaciones optimistas.
- Zustand (con middleware de
- Entorno de ejecución de backend y hosting de APIs: Controladores de rutas de Next.js (
app/api/...) alojados en Google Cloud Run en la misma región que Firestore y el almacenamiento en caché de contexto de Gemini para una latencia interna inferior a 15 ms. - Autenticación: Firebase Authentication (Acceso con Google, correo electrónico/contraseña y vinculación de incorporación anónima). El cliente adjunta
Authorization: Bearer <Firebase_ID_Token>en todas las solicitudes a la API; el servidor verifica la identidad confirebase-admin.auth().verifyIdToken(token). - Base de datos principal: Google Cloud Firestore.
1. Descripción general del sistema y recorrido del usuario principal
1.1 Principios de alcance y arquitectura
Esta especificación define los modelos de datos, los esquemas de Cloud Firestore, los contratos de extremos de servidor y las estructuras de memoria de los agentes de IA para la aplicación Smart Fitness. Establece una fuente única de información para toda la planificación de entrenamientos, el registro histórico de sesiones y la memoria de entrenamiento autónomo por niveles, completamente desacoplada de los frameworks de presentación de frontend (que se abordan en una especificación de diseño de IU/UX dedicada).
1.2 Principios básicos de los datos
- Separación de responsabilidades: Límite estricto entre los contratos de datos (backend/base de datos) y el diseño de la IU del cliente.
- Arquitectura de biblioteca híbrida: Enciclopedia de ejercicios estática e inmutable (
exercises) junto con la composición dinámica de IA de los programas semanales (weekly_plans). - Mutaciones por etapas con intervención humana: Las alteraciones de los entrenamientos generadas por IA se devuelven como propuestas por etapas; las escrituras en la base de datos se producen exclusivamente tras la aceptación explícita del usuario.
- Memoria viva continua de 3 niveles: La IA captura, actualiza y borra de forma autónoma los datos del usuario en elementos básicos permanentes, estados fisiológicos que vencen (con TTL explícito) y contexto situacional transitorio.
- Lecturas detalladas de documentos individuales: Los datos se estructuran en documentos optimizados (como cuadernos de espiral de planes semanales y buckets de chat semanales) para minimizar la latencia de lectura y eliminar las consultas redundantes.
2. Extremos del servidor y contratos de API
Cada extremo debe especificar la fuente de respuesta (
AGENT,DATABASEoHYBRID) para que los extremos respaldados por agentes se puedan configurar de forma adecuada durante la implementación.
POST /api/coach/chat
- Propósito: Interacción conversacional con el entrenador personal de IA, manejo de preguntas de entrenamiento, apoyo motivacional y generación de propuestas de modificaciones de entrenamiento (tanto ajustes diarios como revisiones semanales).
- Activador: El usuario envía un mensaje en la pestaña Entrenador o presiona un chip de edición sugerida contextual intercalado (p.ej., "Reemplazar press Arnold por press de hombros con mancuernas").
- Fuente de la respuesta:
AGENT— Aprovecha Gemini con capacidades de transmisión y llamadas a funciones o herramientas estructuradas. No muta directamente la base de datos, sino que devuelve una propuesta en etapa de pruebas para que la revise un humano. - Encabezados:
Authorization: Bearer <Firebase_ID_Token>,Content-Type: application/json
Carga útil de la solicitud:
{
"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)"
}
}Carga útil de la respuesta:
{
"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)"
}
]
}- Escritura doble en
weekly_chats: En cada turno, el servidor conserva el mensaje entrante del usuario (text) y la respuesta del entrenador (text) en/users/{userId}/weekly_chats/{week_start_date}conFieldValue.arrayUnion(). - Patrón de mutación en etapas: Devuelve la carga útil de la propuesta sin modificar
/weekly_planshasta que el usuario presiona [Accept Plan]. - Administración continua y autónoma de la memoria: Emite
memory_mutationspara crear, actualizar o borrar elementos del perfil de 3 niveles del usuario.
POST /api/workouts/log
- Propósito: Conserva una sesión de entrenamiento completada en el archivo de entrenamiento permanente del usuario, calcula los agregados finales de volumen y repeticiones, verifica los récords personales y marca el día programado como completado.
- Activador: El usuario presiona "Finalizar entrenamiento" en la pantalla de seguimiento del entrenamiento.
- Fuente de la respuesta:
DATABASE, transacción o escritura directa en la base de datos de Cloud Firestore.
Carga útil de la solicitud:
{
"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)"
}Carga útil de la respuesta:
{
"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. Arquitectura de la base de datos de Cloud Firestore
Colección: exercises
Jerarquía: Colección raíz (/exercises/{exercise_id}) • Propósito: Enciclopedia estática de ejercicios que contiene contenido multimedia, indicaciones de forma, requisitos de equipo e instrucciones.
{
"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)"]
}Colección: weekly_plans
Jerarquía: /users/{userId}/weekly_plans/{week_start_date} • Propósito: El "cuaderno espiral" que contiene el programa de entrenamiento semanal completo, las métricas de resumen y los entrenamientos de cada día.
{
"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"
}
]
}
]
}
}
}Colección: weekly_chats
Jerarquía: /users/{userId}/weekly_chats/{week_start_date} • Propósito: El "Weekly Chat Bucket" que contiene el historial de conversaciones completo para una semana de entrenamiento determinada en una sola lectura de 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)"
}
}
]
}Colección: workout_logs
Jerarquía: /users/{userId}/workout_logs/{log_id} • Propósito: Archivo histórico permanente e inmutable de las sesiones de entrenamiento completadas.
{
"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)"
}Colección: user_profiles
Jerarquía: /users/{userId}/profile/main • Propósito: El "Gráfico de cliente vivo" que almacena el perfil de memoria de 3 niveles del usuario.
{
"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"
}
]
}Colección: prs
Jerarquía: /users/{userId}/prs/{exercise_slug} • Propósito: Almacena los récords personales de todos los tiempos, los puntos de referencia estimados de 1 repetición máxima y los récords específicos de repeticiones.
{
"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 de vida de los datos y canalización de agentes
4.1 Canalización 1: Entrenamiento, generación de entrenamientos y modificación por etapas
- Acción del usuario: Envía una entrada de formato libre o presiona un chip de edición contextual generado previamente.
- Ensamblaje de contexto: El servidor recupera el perfil de memoria de 3 niveles activo y el plan de la semana actual, y elimina los elementos de memoria vencidos.
- Inferencia del agente: Gemini procesa la instrucción y el contexto con llamadas a funciones o herramientas estructuradas.
- Mutación autónoma de la memoria: Los datos extraídos del usuario se aplican a
/user_profiles/{userId}y se sincronizan conequipment_ids. - Preparación de la propuesta preparada: Las modificaciones propuestas para el entrenamiento se devuelven con
proposal.status = "pending"dentro de/weekly_chats/{week_start_date}. - Resolución y persistencia del usuario: Cuando se presiona [Accept Plan], se confirman los datos propuestos en
/weekly_plansy se marca la propuesta como"accepted".
4.2 Canalización 2: Ejecución y archivado de entrenamientos (verdad fundamental)
- Grabación de sesiones en vivo: El cliente hace un seguimiento de la carga real levantada, las repeticiones completadas y los indicadores de finalización en el estado de la IU local.
- Envío de la sesión: El cliente llama a
POST /api/workouts/logen "Finalizar entrenamiento". - Cálculo y validación de agregados: El servidor calcula el volumen acumulado de la sesión y verifica los PR con la fórmula de Brzycki.
- Persistencia inmutable: Escribe un registro inmutable en
/workout_logs, marca el día"completed"en/weekly_plansy actualiza o inserta nuevas PR en/prs.
4.3 Canalización 3: Ingesta y anulación de memoria autónoma
- Transferencia: Los hechos se extraen de forma continua en todas las interacciones de conversación.
- Clasificación: Nivel 1 (básicos, sin vencimiento), nivel 2 (estado fisiológico, TTL de 3 a 4 días), nivel 3 (contexto situacional, TTL basado en eventos).
- Eliminación y anulación: Los elementos vencidos (
current_timestamp > expires_at) se filtran del contexto de la instrucción y se borran de forma diferida.
5. Reglas de seguridad de Firestore y especificaciones de índices compuestos
5.1 Reglas de seguridad (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 Índices de consultas compuestas (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. Registro de cambios y decisiones bloqueadas
| Fecha | Component / Decision | Tipo | Bloqueado por |
|---|---|---|---|
| Configuración inicial | Se inicializó el andamiaje de especificación vacío | Configuración del sistema | Equipo |
| 2026-09-04 | Se agregaron las colecciones de Firestore: exercises y weekly_plans | Esquema | Equipo |
| 2026-09-04 | Se agregó el extremo POST /api/coach/chat y la colección chat_messages | Extremo y esquema | Equipo |
| 2026-09-04 | Se refactorizó el almacenamiento de chats al esquema de buckets weekly_chats | Esquema | Equipo |
| 2026-09-04 | Se agregó el extremo POST /api/workouts/log y la colección workout_logs | Extremo y esquema | Equipo |
| 2026-09-04 | Se agregó el esquema de memoria de 3 niveles user_profiles y se actualizó POST /api/coach/chat con memory_mutations | Extremo y esquema | Equipo |
| 2026-09-04 | Haber completado la sección 1 (Alcance y principios del sistema) y la sección 4 (Ciclo de vida y canalizaciones de datos) | Finalización de especificaciones completas | Equipo |
| 2026-09-22 | Especificaciones armonizadas: pila de Next.js + Tailwind + Zustand, autenticación de portador, subcolección de PR, reglas de Firestore y slugs de ejercicios canónicos | Armonización de especificaciones completa | Revisión de la arquitectura |