Files
haptika/Docs/firestore-architecture.md
2026-09-09 10:12:49 +02:00

13 KiB
Raw Permalink Blame History

Architecture Firestore Haptika

État vérifié le 8 septembre 2026 : accès CLI au projet haptika-d13d3 (numéro 167940133580), base (default) Firestore Native en eur3. Mise en place engagée : application iOS enregistrée, SDK natifs Auth/Firestore intégrés dans Expo, règles et index déployés. Le périmètre livré et les étapes restantes sont décrits dans firebase/README.md. Les sections ci-dessous restent la cible d'architecture ; elles ne signifient pas que tous les capteurs et calculs sont branchés.

Organisation retenue

Deux applications, un même compte Firebase Authentication et un historique commun. L'UID Firebase est la frontière d'accès ; ni l'adresse e-mail ni l'identifiant matériel ne servent de clé utilisateur. La Watch passe par le compagnon iPhone avec WatchConnectivity ; son fonctionnement et ses vibrations restent locaux. L'intégration WatchConnectivity dans le nouveau projet Expo reste à construire.

users/{uid}                             profil et état du compte
  onboarding/current                    réponses et étape d'onboarding
  preferences/general                   préférences communes
  practiceSettings/{practiceId}         réglages respiration, ancrage, méditation…
  devices/{installationId}              installations et état de synchronisation
  measurements/{measurementId}          résumé d'une acquisition
  measurementAnnotations/{measurementId} ressenti et contexte déclaratif
  sessions/{sessionId}                  séance et références avant/après
  reminders/{reminderId}                intention de rappel/réveil
  dailyStats/{dayKey}                   agrégats reconstruisibles, serveur
  baselines/{baselineKey}               référence personnelle versionnée, serveur
  entitlements/current                 abonnement vérifié, serveur
  tombstones/{entityKey}                suppressions anti-résurrection, serveur

protocols/{protocolId}/versions/{version} paramètres publiés, serveur

Les sous-collections sont créées à la première écriture ; pas de documents vides pour simuler des tables. Une mesure ou une séance est un document indépendant. Aucun tableau d'historique croissant dans users/{uid}.

Contrat des documents

Tous les documents persistés portent schemaVersion: 1. Les instants sont des Firestore Timestamp UTC ; createdAt et updatedAt sont des timestamps serveur. startedAt, endedAt, recordedAt décrivent le moment sur l'appareil et sont validés indépendamment. Les IDs d'événements sont des UUID aléatoires créés hors ligne puis réutilisés à chaque tentative. Les durées sont en secondes, le rythme cardiaque en bpm et les intervalles en millisecondes. Une valeur indisponible est null, jamais un zéro inventé.

Document Champs métier principaux
Profil firstName, birthYear nullable, gender nullable, locale, timeZone IANA. accountState (active, deleting) est contrôlé par le serveur. E-mail et fournisseurs de connexion restent dans Auth.
Onboarding stress, sleep, energy (low, medium, high), experience (phone, watch), completedAt, flowVersion. L'étape courante utilise une clé métier stable.
Préférences Préférences de notification et de sauvegarde Santé. Une préférence ne prouve jamais une autorisation iOS ; les permissions sont relues sur l'appareil.
Réglages de pratique durationSeconds, paramètres de respiration, intensité, son et écran éteint selon la pratique. Schéma fermé différent par pratique ; pas de dictionnaire arbitraire contenant tous les IDs Figma.
Installation platform, appVersion, pairedInstallationId nullable, lastSeenAt, lastSyncedAt. Identifiant aléatoire d'installation, pas de numéro de série. L'état « connecté » vient de WatchConnectivity, pas de Firestore.
Mesure source, deviceId, startedAt, endedAt, durationSeconds, averageBpm, minimumBpm, maximumBpm, estimatedRmssdMs, sdnnMs, quality, coverage, valid, invalidReasons, algorithmVersion, stressResult nullable. stressResult garde statut, confiance, limitations et version de référence utilisée.
Annotation feeling entier 1–5 nullable, posture, atRest, recentExercise, recentCaffeine, recentMeal, recordedAt, revision. Modification indépendante du résultat acquis.
Séance practiceId, protocolVersion, deviceId, source, startedAt, endedAt nullable, plannedDurationSeconds, actualDurationSeconds, status, beforeMeasurementId, afterMeasurementId, feelingBefore, feelingAfter, résumé de cohérence nullable, revision.
Rappel kind, localTime (HH:mm), weekdays (1=lundi…7=dimanche), timeZone, enabled, targetDeviceId. Planification réelle par l'OS, avec gestion explicite des changements de fuseau et d'heure d'été.
Abonnement plan, status, expiresAt, provider, verifiedAt. Écrit uniquement après validation serveur des achats ; choisir le parcours Watch ne donne aucun droit payant.

Les sources canoniques de mesure sont iphoneCameraPPG et appleWatchOptical, déjà utilisées dans le contrat Watch. Un éventuel import HealthKit doit avoir sa propre source, provenance et méthode. Ne pas confondre PRV estimée par caméra, mesure optique de BPM et HRV HealthKit ; ne pas remplir des métriques absentes de la Watch à partir des exemples Figma. Qualité et couverture sont normalisées entre 0 et 1. Chaque algorithme conserve sa version et son statut de disponibilité.

Les séances utilisent inProgress, awaitingAfter, completed, cancelled. beforeMeasurementId et afterMeasurementId restent des IDs relatifs au même utilisateur ; la transition finale valide leur existence et leur provenance. Une séance sans mesure après ne reçoit pas de comparaison fabriquée.

Synchronisation et conflits

  1. Acquisition et minuteur fonctionnent localement ; aucune écriture par battement ni par seconde. Résumé à la fin, puis mises à jour métier limitées.
  2. La file d'envoi locale est persistante et partitionnée par UID. Les anciennes données sans propriétaire restent locales jusqu'à une association explicite.
  3. La Watch envoie son UUID original ; l'iPhone le conserve en Firestore. Les renvois créent au plus un résultat. Le résumé d'acquisition devient immuable ; une annotation utilise son document séparé.
  4. Les modifications de séances/annotations utilisent une revision monotone validée en transaction. Une révision périmée provoque relecture et résolution, jamais un écrasement silencieux basé sur l'horloge de la Watch.
  5. L'accusé de réception Watch distingue « enregistré durablement sur l'iPhone » et « synchronisé dans le cloud ». La disparition du réseau ne perd pas la file.
  6. Une suppression passe par une opération serveur idempotente : tombstone déterministe (measurement_UUID ou session_UUID), suppression du contenu et invalidation des agrégats. Toute recréation avec cet ID est refusée. Les tombstones restent jusqu'à suppression du compte ; pas de TTL qui permettrait à une vieille Watch hors ligne de ressusciter une mesure.
  7. À la déconnexion/changement de compte, isoler caches, listeners et files ; la Watch doit être réassociée avant d'envoyer au nouvel UID. Un message Watch ne peut pas choisir arbitrairement l'UID de destination.

L'intégration Expo privilégiera les SDK natifs Firebase compatibles avec le development build pour Auth et le cache Firestore. La file métier reste nécessaire pour WatchConnectivity et les transactions qui exigent le réseau. Aucun compte anonyme automatique tant que le parcours de rattachement/récupération n'est pas défini ; le mode local peut fonctionner sans connexion.

Historique, index et coût

Écran / besoin Requête prévue Index composite
Dernières mesures measurements orderBy(startedAt desc) limit(30) + curseur Index simple
Mesures par appareil/source where(source == …) orderBy(startedAt desc) source ASC, startedAt DESC
Historique de pratique sessions where(practiceId == …) orderBy(startedAt desc) practiceId ASC, startedAt DESC
Séances terminées par période sessions where(status == completed) orderBy(startedAt desc) + bornes status ASC, startedAt DESC
Graphique mensuel dailyStats borné par date et fuseau d'agrégation Selon requête finale

Les requêtes partent toujours de users/{uid}. Pas de listener sur tout l'historique et pas de requête globale collectionGroup côté utilisateur. Les cartes d'accueil écoutent seulement les documents nécessaires. Exclure de l'indexation les maps de résultats et les champs jamais interrogés.

Les agrégats sont une optimisation ultérieure, pas un prérequis du premier branchement : lire d'abord une période bornée. Lors de leur introduction, dailyStats porte localDate, timeZone, aggregationVersion, computedAt et des métriques séparées par source. La clé inclut la date et une clé de fuseau stable ; un changement de fuseau invalide/recalcule la vue au lieu de mélanger des journées. Les baselines sont séparées par source, méthode et version d'algorithme. Les traitements serveur doivent être idempotents face aux événements dupliqués et gérer modification/suppression ; un simple incrément par trigger n'est pas suffisant. Un résultat historique garde la référence utilisée à sa date.

Données qui restent locales

Les frames, textes, traductions, icônes et valeurs de démonstration Figma restent dans l'application. Les données démo ne migrent jamais dans l'historique réel. Les échantillons PPG bruts, vidéos caméra, timelines haute fréquence et données HealthKit détaillées ne sont pas téléversés par défaut. Si une fonction justifie ultérieurement leur conservation distante, utiliser un stockage de fichiers privé avec durée de conservation explicite et une référence dans Firestore, plutôt qu'une série volumineuse dans chaque document de mesure.

Politique d'accès à implémenter et tester

  • Refus par défaut. Auth obligatoire et request.auth.uid == uid pour les documents privés, avec vérification que le compte est actif.
  • Autoriser des collections et des champs nommés explicitement ; pas de règle générique users/{uid}/{document=**} autorisant toute écriture.
  • Vérifier types, enums, tailles, plages, timestamps et transitions. Les champs imbriqués nécessitent aussi leurs propres listes de champs autorisés.
  • Le client peut créer son profil avec accountState: active, puis ne modifier que les champs de profil. Il ne peut pas réactiver un compte en suppression.
  • Le client crée une acquisition valide au sens du schéma, puis ne peut plus réécrire ses métriques. Les annotations et réglages sont modifiables selon leur contrat. « Schéma valide » ne signifie pas mesure certifiée par serveur.
  • Abonnements, agrégats, références calculées, tombstones et catalogue de protocoles : écriture serveur uniquement. Protocoles publiés lisibles selon le parcours produit ; accès aux données privées strictement individuel.
  • Les références avant/après restent sous le même UID. La suppression interdit toute recréation et corrige les séances qui référencent la mesure supprimée.
  • La suppression de compte bloque d'abord les écritures, purge récursivement les sous-collections et fichiers associés, puis supprime Auth. Effacer le seul document users/{uid} ne supprime pas ses sous-collections.
  • Tests Emulator requis : non-connecté refusé, utilisateur A/B isolés, écriture d'abonnement refusée, champs inconnus refusés, timestamps falsifiés refusés, mauvaise révision refusée, rejeu idempotent, recréation après suppression refusée, blocage pendant suppression du compte.

Ordre d'intégration

  1. Enregistrer l'app iOS Expo (com.haptika.mobile.dev) dans le projet, configurer Auth selon le parcours retenu, puis intégrer le SDK natif au development build.
  2. Écrire les règles et tests Emulator du contrat minimal : profil, préférences, mesures, annotations et séances ; valider avant déploiement.
  3. Brancher l'état Expo via des repositories métier. Mapper les clés Figma actuelles vers des noms métier stables. Ne pas synchroniser values et toggles tels quels et ne pas importer les faux profils de maquette.
  4. Porter le pont WatchConnectivity dans Expo et tester hors ligne, rejeux, changement de compte et suppression sur simulateurs/appareils adaptés.
  5. Ajouter validation d'achat serveur, agrégats et catalogue distant uniquement lorsque leurs fonctions correspondantes sont branchées.

Sources techniques

Contrats existants examinés : ios/HaptikaWatch/Core/WatchModels.swift, ios/HaptikaWatch/Core/WatchStore.swift, src/features/ppg/storage/storageTypes.ts, src/features/ppg/stress/stressTypes.ts, src/features/coherence/coherenceTypes.ts, src/features/cardiacMirror/regulationSessionTypes.ts, src/features/onboarding/onboardingRepository.ts, mobile/src/state/app-state.tsx.