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

191 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.