13 KiB
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
- Acquisition et minuteur fonctionnent localement ; aucune écriture par battement ni par seconde. Résumé à la fin, puis mises à jour métier limitées.
- 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.
- 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é.
- Les modifications de séances/annotations utilisent une
revisionmonotone 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. - 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.
- Une suppression passe par une opération serveur idempotente : tombstone
déterministe (
measurement_UUIDousession_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. - À 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 == uidpour 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
- 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. - Écrire les règles et tests Emulator du contrat minimal : profil, préférences, mesures, annotations et séances ; valider avant déploiement.
- Brancher l'état Expo via des repositories métier. Mapper les clés Figma
actuelles vers des noms métier stables. Ne pas synchroniser
valuesettogglestels quels et ne pas importer les faux profils de maquette. - Porter le pont WatchConnectivity dans Expo et tester hors ligne, rejeux, changement de compte et suppression sur simulateurs/appareils adaptés.
- 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
- Bonnes pratiques et indexation
- Contrôle des champs par les règles
- Persistance hors ligne
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.