# 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. ```text 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 - [Modèle Firestore et sous-collections](https://firebase.google.com/docs/firestore/data-model) - [Bonnes pratiques et indexation](https://firebase.google.com/docs/firestore/best-practices) - [Contrôle des champs par les règles](https://firebase.google.com/docs/firestore/security/rules-fields) - [Persistance hors ligne](https://firebase.google.com/docs/firestore/manage-data/enable-offline) 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`.