J’ai créé un espace d’échanges sécurisés
La porte d’entrée : un mot de passe, puis un code à usage unique.
J’ai mis en production un espace d’échanges sécurisés qui remplacera les mails. Il est en ligne à l’adresse espace.clavelier.me, servi par mon serveur, et repose sur Supabase.
Je le présente ici en détail : pourquoi il existe, comment il est construit, ce qu’on y fait, et ce qui peut venir ensuite. Avec un peu de code au passage, celui qui tourne réellement.
Pourquoi sortir du mail
Un mail est une enveloppe ouverte. Presque toujours chiffré en transit, il reste lisible par chaque serveur qu’il traverse : je l’ai détaillé dans Mail : la lettre qui voyage ouverte. Pour un devis, un document de conception ou une décision, ce n’est pas à la hauteur.
L’espace répond à trois besoins.
Sécuriser les échanges. Rien ne circule en clair : tout passe par une connexion chiffrée, vers un espace fermé où n’entrent que les personnes du projet.
Tracer les échanges et les décisions. Chaque document, chaque message, chaque validation est daté et conservé : on sait qui a dit quoi, et quand.
Valider les devis et les documents de conception. Le client approuve un document, ou le refuse avec un motif, sur sa version exacte.
Un point qui compte : rien n’est automatisé. Chaque réponse, chaque échange se fait à la main, de mon côté. Il y a un artisan derrière cet espace.
Une valise, et sa démonstration
Cet espace est aussi une valise numérique : il montre ce qu’on peut construire. Le principe de la valise : les données et les règles qui les gouvernent vivent ensemble dans la base.
Toute la sécurité repose donc sur Supabase, c’est-à-dire sur PostgreSQL et ses règles RLS (Row Level Security, la sécurité ligne par ligne). La documentation de Supabase en donne une image simple : une politique revient à ajouter une clause WHERE à chaque requête. Quoi que demande l’interface, la base ne rend que les lignes que la personne connectée a le droit de voir.
L’interface, générée par un LLM, est hébergée sur mon serveur : du HTML, du CSS et du JavaScript servis tels quels, sans framework ni étape de construction. Elle ne contient aucun secret. La seule clé qu’elle porte est la clé publiable du projet, faite pour être publique : elle identifie le projet, elle n’ouvre rien. La même documentation le précise : avec cette clé, aucune donnée n’est accessible par l’API tant qu’aucune politique ne l’autorise. Mon rôle d’administrateur, côté interface, ne sert qu’à afficher ou non les formulaires de gestion : ce qui est permis, c’est la base qui le décide.
L’interface peut changer de forme ou de LLM : la sécurité, elle, reste dans la base.
La double authentification, pour tout le monde
Pour entrer, j’impose la double authentification : un code à usage unique, affiché par une application sur le téléphone, en plus du mot de passe. Ce sont deux facteurs de nature différente : ce que l’on sait (le mot de passe) et ce que l’on a (le téléphone). La CNIL recommande de privilégier l’authentification multifacteur lorsque cela est possible, en particulier quand la connexion est accessible depuis l’extérieur, ce qui est le cas d’un espace en ligne.
Ce n’est pas une contrainte de confort, c’est une hygiène de base dès qu’on manipule des données de travail. Et c’est de toute façon indispensable : le client aura son propre compte Supabase pour son projet, où ce second facteur est la norme. Autant le prendre dès le début.
Côté technique, Supabase inscrit le niveau d’authentification dans le jeton de session : aal1 après le seul mot de passe, aal2 une fois le second facteur vérifié. Chaque table porte alors une règle de plus :
create policy double_authentification on public.documents
as restrictive to authenticated
using ((select auth.jwt()) ->> 'aal' = 'aal2');
Le mot qui compte est restrictive. Dans PostgreSQL, les politiques permissives se combinent par un OU, les restrictives par un ET : les premières ouvrent des accès, les secondes posent des conditions que tout accès doit remplir. Une session sans code à usage unique ne voit donc rien, même avec le bon mot de passe, quelle que soit la règle qui lui ouvrirait l’accès.
La même règle dans le tableau de bord de Supabase, sur la table des documents.
Organisations, membres, projets
L’espace repose sur trois briques. L’organisation, c’est la structure cliente. Les membres, ce sont les personnes qui s’y connectent. Les projets appartiennent à une organisation, qui peut en avoir plusieurs. Chaque membre voit ses organisations et leurs projets, rien d’autre ; une même personne peut appartenir à plusieurs organisations.
Personne ne s’inscrit seul : les inscriptions publiques sont fermées dans Supabase, et une tentative d’inscription par l’API est refusée. Je crée l’organisation et ses projets, puis j’invite chaque membre par son adresse mail. Il reçoit un lien pour créer son compte, choisit son mot de passe et enregistre son second facteur : sans invitation, pas de compte.
Mon rôle d’administrateur est porté par le jeton, dans app_metadata, une zone que l’utilisateur ne peut pas modifier, contrairement à user_metadata. Et il ne vaut qu’avec le second facteur. La fonction est_admin(), sur laquelle s’appuient les règles, tient en une requête :
select coalesce(auth.jwt() -> 'app_metadata' ->> 'role' = 'admin'
and auth.jwt() ->> 'aal' = 'aal2', false);
Le reste découle d’une règle unique : on voit un projet si l’on est membre de son organisation, et on voit un document, un message ou un fait si l’on voit son projet. Elle s’écrit presque comme elle se dit :
create policy lecture on public.documents for select to authenticated
using (projet_id in (select id from public.projets));
La sous-requête sur projets passe elle-même par les règles de projets : elle ne rend que les projets visibles par la personne connectée. Aucune règle n’est recopiée d’une table à l’autre, elles s’enchaînent.
Le client, lui, tient à jour l’adresse de facturation de son organisation, pour les devis et les factures, et son profil : nom et téléphone, pour que je puisse le contacter.
Ensuite, tout se passe dans l’espace, rattaché au projet, au fil de son évolution dans le temps. Une évolution demandée plus tard reste sur le même projet, dont le statut repasse à « en création » : son historique ne se perd pas. Et un projet ne change jamais d’organisation, sans quoi son historique passerait à une autre.
Le fil d’un projet
Dans chaque projet, on retrouve trois onglets : la chronologie, les documents et les messages.
La chronologie, c’est le fil du projet, jour par jour. Chaque fait y est inscrit : la création du projet, un changement de statut, un document déposé, un message, une validation ou un refus, et les actions que j’y note moi-même. L’auteur et la date sont fixés par le serveur : depuis l’espace, personne ne peut antidater un fait ni l’attribuer à un autre.
Rien ne s’y modifie ni ne s’y supprime. Ce n’est pas une promesse de l’interface, c’est un trigger posé sur la table, qui refuse toute modification et toute suppression, y compris depuis le tableau de bord de Supabase :
create trigger evenements_immuables before delete or update on public.evenements
for each row execute function prive.interdire_modification();
La fonction appelée se contente de lever une erreur : « une ligne ne se modifie ni ne se supprime ». Un second trigger fait de même pour le vidage complet de la table. Les messages sont protégés de la même façon.
Chaque fait est marqué lu ou non lu. Quand un membre ouvre le projet, je sais qu’il les a vus, et quand. Et inversement. Le fait lui-même n’est jamais modifié pour autant : la lecture s’inscrit dans une table à part, une ligne par fait et par personne, et seule la première ouverture compte.
L’interface ajoute une mise en forme : une bulle par fait, l’autre partie d’un côté du fil, soi de l’autre, et ce qui est nouveau en couleur.
La chronologie d’un projet, vue du client. Compte et projet de test floutés.
Les documents et leur validation
Les documents se déposent des deux côtés, rangés par projet puis par type : devis, facture, conception, livrable, divers. Ils vivent dans un espace de stockage privé de Supabase, et un fichier n’y est lisible que si son document l’est, donc par les seules personnes du projet.
À chaque dépôt, l’interface calcule l’empreinte SHA-256 du fichier avant de l’envoyer. Une empreinte est une suite de 64 caractères tirée du contenu : le même fichier donne toujours la même, et le moindre octet changé en donne une autre. Le navigateur sait la calculer seul :
async function sha256(fichier) {
const empreinte = await crypto.subtle.digest('SHA-256', await fichier.arrayBuffer());
return [...new Uint8Array(empreinte)].map((octet) => octet.toString(16).padStart(2, '0')).join('');
}
Le fichier est envoyé, puis déclaré à la base avec son empreinte : c’est cette déclaration qui crée le document et l’inscrit dans la chronologie. Un document déposé ne se remplace pas et ne se supprime pas ; seuls son nom et sa description se corrigent.
Quand je dépose un document, je peux demander sa validation. Le client le télécharge, puis le valide ou le refuse, avec un motif s’il le souhaite. Avant qu’il confirme, l’interface le prévient : en validant, il donne son accord et s’engage au nom de son organisation, sur cette version exacte du fichier, et cet accord est daté.
La « version exacte » se vérifie. Au moment de valider, l’interface retélécharge le fichier stocké, recalcule son empreinte et l’envoie à une fonction de la base, qui la compare à celle enregistrée au dépôt :
const fichier = await ok(sb.storage.from('documents').download(d.chemin));
await ok(sb.rpc('valider_document', { document: d.id, hash_fichier: await sha256(fichier) }));if hash_fichier is distinct from d.hash then
raise exception 'Le fichier ne correspond pas à la version soumise';
end if;
Cette fonction s’exécute avec des droits étendus, pour pouvoir écrire dans la chronologie : elle contrôle donc elle-même l’appelant (second facteur, appartenance à l’organisation du projet). Elle refuse aussi une seconde réponse : une seule validation ou un seul refus par document, sans retour. Pour une nouvelle version, je dépose un nouveau document. La validation et le refus entrent dans la chronologie, avec l’empreinte du fichier.
Les messages, et les mails de notification
Les messages, enfin : une messagerie par projet, entre ses membres et moi. On n’écrit que dans les projets qu’on voit, un message peut renvoyer à un document du même projet, et un message envoyé ne se modifie plus.
La messagerie du même projet, vue du client. Compte et projet de test floutés.
Le mail ne disparaît pas tout à fait : il sert à prévenir, jamais à transporter. Toutes les heures, une tâche pg_cron appelle par pg_net (deux extensions présentées dans Supabase avancé) une Edge Function, qui envoie au plus un mail à chaque personne ayant du nouveau non lu. Ce mail liste les projets et le nombre de nouveautés, sans rien dévoiler de leur contenu : pour lire, il faut se connecter. Ce qu’on a déjà ouvert dans l’heure ne part pas, et chaque fait n’est signalé qu’une fois.
Éprouvé avant d’ouvrir
Le backend est couvert par 252 tests de sécurité et de fonctionnement, à rejouer à chaque changement. Un test d’intrusion externe a été mené par un agent autonome dans une machine virtuelle jetable, en anonyme puis en membre authentifié avec son second facteur : le backend n’a montré aucune faille, seuls les en-têtes HTTP du serveur ont été durcis. Les parcours ont été faits sur le site en ligne avec un compte client de test : invitation, mot de passe oublié, dépôt, validation, refus avec motif.
Ce qui peut venir
L’ensemble s’enrichit facilement, côté interface comme côté valise. Un bouton pour prendre rendez-vous avec moi, ce sont quelques lignes dans l’interface.
Plusieurs pistes sont ouvertes :
- La signature des documents par un tiers. Aujourd’hui, la validation est un accord daté sur une version exacte, dont la preuve est gardée par mon propre outil. Une signature électronique confie cette preuve à un prestataire indépendant, dont c’est le métier (Docusign, Yousign ou Universign, par exemple) : il garantit l’identité du signataire et l’intégrité du document.
- Des tickets pour les outils numériques que je gère : un problème signalé, suivi et résolu au même endroit que le reste du projet.
- Des formulaires, pour recueillir auprès d’un client les informations qui manquent.
Un cas particulier, un besoin nouveau s’ajoutent à l’espace sans tout reprendre. C’est l’avantage du sur-mesure.
Pour moi, l’espace réunira le suivi de mes projets. Pour le client, il garde la mémoire du sien.