Messagerie clinique

Tout ce que la messagerie Synapse sait faire

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.

Types de fils Types de messages Aperçu d'un fil Groupes & rôles Accusés de lecture Chaîne de chiffrement Garanties
Types de fils

Cinq formats pensés pour la pratique clinique

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é.

direct

Échange 1:1

Entre deux professionnels

Conversation privée — référé patient, demande d'avis, transmission de dossier.

group

Groupe de soins

Équipe pluridisciplinaire

Tous les membres peuvent écrire. Service de cardio, équipe de garde, étage d'oncologie.

channel

Canal de diffusion

Établissement → praticiens

Émission descendante. Membres en lecture seule. Bulletins, protocoles, alertes internes.

Lecture seule
rcp

Réunion de Concertation Pluridisciplinaire

Discussion de dossier complexe

Chirurgien, oncologue, radiologue. Champ subject = référence anonymisée du dossier.

Sujet anonymisé
alert

Alerte / Pharmacovigilance

Urgence sanitaire

Émission ciblée avec accusé de réception obligatoire. Centre Anti-Poison, alerte épidémique.

Accusé obligatoire
Types de contenu

Du texte simple au document structuré

Synapse 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é.

💬

Texte

Message standard chiffré AES-256-GCM. Pas de limite de taille raisonnable, support Unicode complet (emojis médicaux, caractères arabes).

📎

Pièce jointe

PDF, image, dossier zippé. Le binaire entier est chiffré côté client avant upload. Métadonnées file_name et file_size conservées.

📋

Document structuré

Résultat NFS, lettre de liaison, ordonnance. Schéma JSON validé, valeurs hors normes surlignées automatiquement à l'affichage.

🎙

Message vocal

Enregistrement court chiffré côté client. Utile en bloc ou en garde quand la saisie est impossible. Prévu phase P1.

📷

Photo médicale

Cliché lésion, ECG papier scanné. Compression côté client, métadonnées EXIF retirées avant chiffrement. Prévu phase P1.

✍️

Signature numérique

Signer une lettre de liaison ou une ordonnance. Couplé au certificat PSC v2 (FR) ou CNOM (MA). Prévu phase P2.

Exemple en situation

Un fil RCP avec les trois types de contenu

Reconstitution fidèle de l'écran ThreadScreen avec composants MessageBubble pour chaque type.

RCP
RCP — Cancer du sein · M. C.
3 participants · ✓ Tous vérifiés CNOM
Dossier transmis pour avis pluridisciplinaire. Voir bilan ci-joint.
Dr. Benali · Chir · 09:12
📄
bilan_pre_op_anonymise.pdf
Chiffré E2E · 1.8 Mo
📋 Résultat NFS
Hémoglobine9.2 g/dL ⚠
Leucocytes6.8 G/L
Plaquettes185 G/L
LabSynlab Rabat
Anémie modérée pré-op. Je propose une transfusion avant chirurgie.
Dr. Saidi · Onco · 09:24 ✓✓
Accord. RCP validée, je rédige le compte-rendu.
Dr. Khalid · Radio · 09:31
Membres & rôles

Groupes : trois rôles, trois niveaux de droits

Chaque participant d'un fil a un rôle qui détermine ce qu'il peut faire — écrire, ajouter des membres, ou simplement lire.

admin

Administrateur

Crée le fil, ajoute / retire les membres, change le titre. Détient les clés enveloppées initiales.

member

Membre

Lit et écrit librement. Reçoit notifications push et accusés de réception.

observer

Observateur

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 filtitreparticipants. À 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).

Accusés de lecture

Trois états, sémantiquement clairs

Composant ReadReceiptIndicator.tsx — inspiré des standards de messagerie mais adapté à l'usage médical (alerte si non-lu sur fil alert).

Envoyé

Le serveur a accusé réception du chiffré.

✓✓

Distribué

Le destinataire a reçu le message sur son appareil.

✓✓

Lu

Le message a été déchiffré et affiché. Horodaté.

⚠ Fils 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.
Chaîne de chiffrement

Le serveur ne voit jamais le clair

Toute la crypto vit dans lib/crypto.ts et lib/secureKeyStore.ts. Web Crypto API natif via Hermes, aucune librairie tierce.

Identité du professionnel — une fois à l'installation

getOrCreateIdentityKeyPair() génère une paire ECDH P-256.

  • Clé privée (PKCS8) → SecureStore (keychain natif)
  • Clé publique (SPKI base64) → envoyée au serveur via PATCH /v1/me/profile
lib/secureKeyStore.ts → getOrCreateIdentityKeyPair()

Création d'un fil — clé AES par fil

generateThreadKey() 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.

lib/crypto.ts → wrapThreadKey() → POST /v1/threads

Envoi d'un message — chiffrement avant le réseau

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.

POST /v1/threads/{id}/messages

Réception temps-réel — WebSocket + déchiffrement local

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é.

lib/api/messagingWs.ts + useThreadKey()

Cache mémoire des clés — jamais sur disque

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.

lib/secureKeyStore.ts
🔒 La constante 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.
Garanties techniques

Ce que Synapse garantit, ligne par ligne

Aucun texte en clair côté serveur

La table messages ne contient que ciphertext bytea et nonce bytea — aucune colonne plaintext.

Clé privée jamais exportée

SecureStore iOS / Android Keystore. Elle ne quitte jamais le terminal du professionnel.

Une clé par fil

La compromission d'un fil n'expose pas les autres conversations.

Nonce aléatoire par message

12 octets tirés via expo-crypto (CSPRNG natif). Pas de réutilisation possible.

Tokens jamais loggés

L'URL WebSocket (qui contient access_token) n'apparaît jamais dans les traces. Logger sanitisé dans core/logging.

Refresh token automatique

Sur 401, apiFetchWithRefresh() rejoue avec un token fraîchement refresh — sans déconnecter l'utilisateur.

RLS sur tout le schéma

Côté backend Supabase legacy : Row-Level Security activée sur toutes les tables, vérifiée à chaque requête.

Audit conforme HDS

Tous les événements (envoi, lecture, ajout de membre) sont tracés dans audit_events. Rétention 10 ans (FR).