Cinq types de fils cliniques, messages texte / fichier / structurés, accusés de lecture médicaux, et un chiffrement de bout en bout audité ligne par ligne.
Là où WhatsApp propose « chat 1:1 » et « groupe », Synapse distingue cinq contextes médicaux avec leurs règles propres : qui peut écrire, qui doit confirmer la lecture, comment le sujet est référencé.
Entre deux professionnels
Conversation privée — référé patient, demande d'avis, transmission de dossier.
Équipe pluridisciplinaire
Tous les membres peuvent écrire. Service de cardio, équipe de garde, étage d'oncologie.
Établissement → praticiens
Émission descendante. Membres en lecture seule. Bulletins, protocoles, alertes internes.
Lecture seuleDiscussion de dossier complexe
Chirurgien, oncologue, radiologue. Champ subject = référence anonymisée du dossier.
Urgence sanitaire
Émission ciblée avec accusé de réception obligatoire. Centre Anti-Poison, alerte épidémique.
Accusé obligatoireSynapse ne traite pas un compte-rendu de biologie comme un message texte : les types de contenu sont explicites, validés, et chiffrés à l'unité.
Message standard chiffré AES-256-GCM. Pas de limite de taille raisonnable, support Unicode complet (emojis médicaux, caractères arabes).
PDF, image, dossier zippé. Le binaire entier est chiffré côté client avant upload. Métadonnées file_name et file_size conservées.
Résultat NFS, lettre de liaison, ordonnance. Schéma JSON validé, valeurs hors normes surlignées automatiquement à l'affichage.
Enregistrement court chiffré côté client. Utile en bloc ou en garde quand la saisie est impossible. Prévu phase P1.
Cliché lésion, ECG papier scanné. Compression côté client, métadonnées EXIF retirées avant chiffrement. Prévu phase P1.
Signer une lettre de liaison ou une ordonnance. Couplé au certificat PSC v2 (FR) ou CNOM (MA). Prévu phase P2.
Reconstitution fidèle de l'écran ThreadScreen avec composants MessageBubble pour chaque type.
Chaque participant d'un fil a un rôle qui détermine ce qu'il peut faire — écrire, ajouter des membres, ou simplement lire.
Crée le fil, ajoute / retire les membres, change le titre. Détient les clés enveloppées initiales.
Lit et écrit librement. Reçoit notifications push et accusés de réception.
Lecture seule — utilisé dans les channel de diffusion. Le composant MessageComposer est désactivé.
Création d'un groupe : wizard en 3 étapes —
type de fil →
titre →
participants. À chaque ajout d'un participant, sa clé publique ECDH est récupérée, et la clé du fil lui est enveloppée individuellement (wrapThreadKey).
Composant ReadReceiptIndicator.tsx — inspiré des standards de messagerie mais adapté à l'usage médical (alerte si non-lu sur fil alert).
Le serveur a accusé réception du chiffré.
Le destinataire a reçu le message sur son appareil.
Le message a été déchiffré et affiché. Horodaté.
alert :
Le champ acknowledgment_required force l'utilisateur à confirmer activement la lecture (un tap explicite). Si non confirmé après le délai défini par l'émetteur, une notification de relance est déclenchée.
Toute la crypto vit dans lib/crypto.ts et lib/secureKeyStore.ts. Web Crypto API natif via Hermes, aucune librairie tierce.
getOrCreateIdentityKeyPair() génère une paire ECDH P-256.
PATCH /v1/me/profilegenerateThreadKey() tire une clé AES-256 fraîche, jamais réutilisée entre fils.
Pour chaque participant : ECDH (sa clé publique × ma clé privée) → HKDF-SHA-256 (info "almoussaid-wrap-v1") → enveloppe AES-GCM de la clé de fil.
encryptMessage(body, key) sérialise le MessageBody (texte / fichier / structuré), tire un nonce 12 octets aléatoire, applique AES-GCM.
Envoie au serveur : { ciphertext_b64, nonce_b64 }. Aucune autre information sur le contenu.
Le WS pousse { type: 'new_message', message: { ciphertext_b64, nonce_b64 } }.
Le client récupère la clé du fil (cache mémoire ou serveur), decryptMessage() retourne le MessageBody. Le serveur n'a jamais déchiffré.
Les clés de fil déchiffrées sont gardées dans une Map en RAM (_threadKeyCache). Effacées à la fermeture de l'app.
Au redémarrage : redéchiffrement de la clé enveloppée via la clé privée ECDH dans SecureStore.
HKDF_INFO = 'almoussaid-wrap-v1' est figée. Toute modification rendrait illisible l'intégralité des clés enveloppées existantes — y compris celles partagées avec le client web.
La table messages ne contient que ciphertext bytea et nonce bytea — aucune colonne plaintext.
SecureStore iOS / Android Keystore. Elle ne quitte jamais le terminal du professionnel.
La compromission d'un fil n'expose pas les autres conversations.
12 octets tirés via expo-crypto (CSPRNG natif). Pas de réutilisation possible.
L'URL WebSocket (qui contient access_token) n'apparaît jamais dans les traces. Logger sanitisé dans core/logging.
Sur 401, apiFetchWithRefresh() rejoue avec un token fraîchement refresh — sans déconnecter l'utilisateur.
Côté backend Supabase legacy : Row-Level Security activée sur toutes les tables, vérifiée à chaque requête.
Tous les événements (envoi, lecture, ajout de membre) sont tracés dans audit_events. Rétention 10 ans (FR).