
Table des matières
Ouvrir table des matières
L’EDI n’est pas mort, et il veut un répertoire
On est en 2026, tout le monde parle d’API REST, de webhooks, d’événements. Et puis un jour, un client vous dit tranquillement : « nous, on s’intègre en EDI. Vous nous donnez un SFTP, on dépose nos fichiers dans un répertoire, on récupère les retours dans un autre. »
Ne souriez pas. L’EDI est partout — ERP historiques, éditeurs métier, grands comptes — et il est là pour longtemps. Un fichier déposé dans un répertoire, c’est l’API des systèmes qui existaient avant les API. La vraie question n’est pas de savoir si vous devez le supporter (si vos clients le demandent, vous le devez), mais comment le supporter sans saboter votre architecture.
Et là, il y a un piège dans lequel je vois plonger beaucoup d’équipes Node : « il nous faut un serveur SFTP ? Très bien, il y a une lib pour ça, embarquons un serveur SSH dans l’application. » Techniquement, ça marche. Architecturalement, c’est une petite catastrophe en devenir. Voici pourquoi j’ai fait l’inverse : un endpoint SFTP par client, sans qu’une seule ligne de mon backend ne parle jamais SSH.
Pourquoi je ne voulais pas d’un démon SSH dans mon backend
Posons les griefs contre le serveur SSH embarqué, parce qu’ils sont sérieux :
- La surface d’attaque. SSH est un protocole d’accès distant. L’embarquer dans votre application, c’est exposer la négociation cryptographique, la gestion des algorithmes, l’authentification par clé… directement dans le processus qui tient aussi votre logique métier et vos connexions base de données. Chaque CVE du protocole devient une CVE de votre application.
- L’exploitation. Rotation des clés d’hôte, algorithmes à déprécier, durcissement de configuration : c’est un métier à part entière, que des outils spécialisés font très bien et que votre app fera mal.
- La haute disponibilité. Un dépôt SFTP est une session avec état sur une connexion longue. Vos replicas applicatifs, eux, sont pensés pour des requêtes courtes et sans état. Marier les deux, c’est réinventer l’équilibrage de sessions pour un protocole qui n’est pas le vôtre.
- Et le pire : le mélange des genres. Le transport (comment les octets arrivent) s’invite dans le métier (ce qu’on fait des documents). Tout ce que l’architecture hexagonale nous apprend à séparer.
Bref, je ne voulais pas héberger du SSH. Je voulais juste que les fichiers de mes clients arrivent. Ce sont deux besoins différents — et le premier n’est pas le mien.
La décision : une passerelle devant, un bucket derrière
La solution tient en une phrase : une passerelle SFTP spécialisée, configurée pour écrire dans un stockage objet. Il existe d’excellents outils open source pour ça (SFTPGo, pour ne pas le nommer) dont c’est précisément le métier : parler SSH/SFTP d’un côté, et ranger les octets reçus dans un backend de stockage de l’autre — un bucket S3-compatible, en ce qui me concerne.
Le partage des rôles devient limpide :
- la passerelle authentifie chaque client (par clé uniquement — pas de mot de passe — avec des identifiants propres à chaque client et aucun compte générique de secours) et dépose chaque fichier reçu dans le bucket sous un préfixe dédié :
inbound/<clientId>/<répertoire>/…; - l’application, elle, ne voit jamais une session SSH de sa vie. Elle consomme le bucket. Point.
Et comment le consomme-t-elle ? C’est le deuxième étage de la fusée, et mon pattern préféré de ce chantier : le bucket-as-queue. Un poller périodique liste le préfixe d’entrée, détecte les nouveautés et les injecte dans le pipeline métier. Le bucket joue le rôle d’une file d’attente durable : les dépôts s’y accumulent même si l’application est en cours de déploiement, rien ne se perd, et le traitement rattrape à son rythme. Ce patron tournait d’ailleurs déjà ailleurs sur la plateforme pour un autre canal d’entrée — la nouveauté ici, c’est la porte, pas la mécanique.

Au passage, vous noterez que le préfixe fait d’une pierre deux coups : c’est aussi lui qui porte la tenance. Le client propriétaire d’un fichier, c’est le premier segment de son chemin de stockage — l’endroit où il a atterri, que nous avons provisionné — et jamais ce que le fichier raconte. J’ai consacré un article entier à cette règle, je n’y reviens pas, mais elle s’applique ici au premier chef : résolution nulle ou ambiguë, refus et quarantaine.
Le ledger est l’autorité, les fichiers sont une projection
Un piège classique des intégrations à base de répertoires : laisser l’arborescence de fichiers être l’état du système. « Si le fichier est dans done/, c’est traité ; s’il est dans error/, c’est en erreur. » Ça semble simple, jusqu’au premier crash entre deux opérations — et vous voilà avec un fichier traité mais pas déplacé, ou déplacé mais pas traité, et aucun moyen de trancher.
Ma règle : la vérité vit en base, dans un ledger ; l’arborescence n’est qu’une projection pour les humains et les clients. Chaque dépôt détecté est enregistré dans un registre avec une clé de déduplication dérivée de son emplacement — c’est elle qui garantit l’exactly-once : si le poller revoit le même fichier (redémarrage, chevauchement, redélivrance), la clé existe déjà, on ne retraite pas. Ensuite seulement, le fichier est déplacé vers son répertoire de destin, en mark-then-move : on marque en base, puis on déplace. Un crash entre les deux ? Au tick suivant, le marquage dit la vérité et le déplacement se rejoue — l’opération est idempotente, le pire scénario est un fichier en retard d’un rangement, jamais un fichier traité deux fois.
// Le squelette du poller — la base décide, le fichier suit.
for (const object of await storage.list(`inbound/${clientId}/`)) {
const dedupeKey = `sftp:${clientId}:${object.path}`;
const claim = await ledger.claim(dedupeKey); // INSERT … ON CONFLICT DO NOTHING
if (!claim.isNew) continue; // déjà vu : redélivrance, on ignore
const outcome = await processDeposit(clientId, object); // le MÊME pipeline que l'API
await ledger.record(dedupeKey, outcome);
await storage.move(object.path, projectionPathFor(outcome)); // la projection, en dernier
}Vous reconnaissez la philosophie des articles précédents : l’état d’autorité est un enregistrement atomique en base, et tout le reste — fichiers, écrans, métriques — n’en est que le reflet. La base de données la plus ennuyeuse reste le meilleur arbitre du monde.
Le même chemin métier que l’API — c’est ça, la vraie victoire
Le plus gros du travail n’a d’ailleurs pas été le SFTP du tout. Ç’a été un refactoring : extraire le pipeline de traitement de la couche HTTP, où il vivait confortablement dans les handlers de routes, pour en faire un use-case injectable et transport-agnostique. L’identifiant du client y est fourni par l’appelant — qui l’a résolu depuis sa propre source de vérité : le jeton pour HTTP, le préfixe pour SFTP — et les issues sont typées, sans texte localisé ni code de statut HTTP dans la couche applicative.
Résultat : un document déposé par SFTP traverse exactement le même chemin métier qu’un document poussé par l’API. Mêmes validations, mêmes règles, mêmes registres, mêmes obligations. Le SFTP n’est pas un « mode dégradé » avec sa propre logique qui divergera dans six mois — c’est un adaptateur de plus devant le même hexagone. Le jour où un client demandera un troisième canal (un partage réseau ? une boîte mail ? qui sait, avec l’EDI), ce sera un adaptateur de plus, pas une réécriture.
Trois raffinements de résilience pour finir le tour du propriétaire :
- Un scheduler de polling par client. Pas un poller global : un par tenant. Si le flux d’un client se met à échouer en boucle ou à déborder, les autres ne voient rien. L’isolation des locataires descend jusque dans la planification.
- Le zip, avec méfiance. Certains clients déposent des archives. Le décodeur travaille en streaming avec des défenses explicites : ratio de décompression plafonné (une zip-bomb s’arrête au plafond, pas à l’épuisement mémoire) et intégrité vérifiée par somme de contrôle.
- Des issues typées, aux destins différents. « Quota dépassé » est un retry-later — jamais terminal, le dépôt attendra son tour (les lecteurs de l’article sur le quota reconnaîtront l’ajournement). Un échec métier avéré, lui, part en quarantaine terminale, visible et motivée. Confondre les deux, c’est soit perdre des documents valides, soit rejouer l’invalide pour l’éternité.
Épilogue
Ce que cette histoire de SFTP m’a confirmé, c’est qu’un besoin « legacy » ne mérite pas une architecture au rabais. Le réflexe « c’est de l’EDI, on va bricoler un truc à côté » produit des chemins parallèles qui divergent, des états ambigus et des démons qu’on n’aurait jamais dû héberger. Le bon mouvement est inverse : traiter le transport comme un détail — passerelle spécialisée, stockage objet, poller idempotent — et faire converger tous les canaux vers l’unique chemin métier.
- Le transport est un adaptateur ; votre backend n’a pas à parler tous les protocoles du monde, seulement à lire un bucket.
- Ledger en base = autorité ; fichiers = projection. Mark-then-move, clés de déduplication, idempotence partout.
- Isolation par tenant jusque dans le scheduling, et méfiance systématique envers ce qui se décompresse.
Pour prolonger : l’architecture hexagonale qui rend ce genre d’extraction naturelle, la résolution de tenant qui est l’autre moitié de cette porte d’entrée, et le quota partagé qui gouverne ce qui ressort de l’autre côté.
Et chez vous, l’EDI arrive comment ? Démon embarqué, passerelle, ou pire (on a tous vu le cron qui scanne un partage Samba) ? Racontez-moi vos portes d’entrée en commentaire — je me ferai une joie de vous répondre.

