Architecture technique
Vue d'ensemble
Vaks PM est une application web de gestion de projet multi-utilisateurs conçue pour fonctionner entièrement sur une infrastructure que l'organisation contrôle. Elle est on-premise, c'est-à-dire hébergée sur les serveurs de l'organisation elle-même plutôt que sur un service en ligne tiers. Le produit est livré sous la forme d'un petit ensemble d'images de conteneurs, et une installation complète n'a aucune dépendance obligatoire à un service en ligne externe.
Le produit entier se résume à une poignée d'images de conteneurs plus trois magasins de données durables (une base de données relationnelle, un cache et un magasin d'objets). Tout le reste dont le système a besoin, comme la répartition de charge, la coordination du basculement et la messagerie temps réel, provient de composants hébergés par l'installation elle-même. Il existe exactement deux connexions sortantes optionnelles, et le système fonctionne sans aucune des deux :
- Un relais de messagerie, SMTP (Simple Mail Transfer Protocol, le protocole standard d'envoi de courrier), utilisé uniquement pour distribuer les e-mails de notification.
- Un fournisseur d'identité d'entreprise tel que Microsoft Entra ID, utilisé seulement si l'authentification unique est requise.
L'application est API-first : l'interface web consomme la même interface REST (Representational State Transfer, le style conventionnel des API web HTTP) publique que celle utilisée par les intégrations tierces. L'autorisation est appliquée côté serveur pour chaque requête et chaque événement temps réel, et l'on ne fait jamais confiance au front end pour contrôler les accès par lui-même. Les services qui portent la logique applicative, l'API et le worker d'arrière-plan, sont sans état (ils ne détiennent aucune donnée locale) et passent donc à l'échelle horizontalement en lançant simplement plus de copies.
Glossaire
Acronymes employés dans tout ce guide, définis une fois ici pour référence.
| Terme | Signification |
|---|---|
| On-premise | Logiciel hébergé sur une infrastructure que l'organisation contrôle, plutôt que sur un service en ligne exploité par un fournisseur. |
| HA | High Availability (haute disponibilité) : une topologie qui survit à la perte d'un hôte unique sans interruption. |
| DNS | Domain Name System (système de noms de domaine), qui associe des noms d'hôtes à des adresses réseau. |
| TLS | Transport Layer Security, la couche de chiffrement derrière HTTPS. |
| VIP | Virtual IP (adresse IP virtuelle) : une adresse réseau unique partagée par plusieurs hôtes, de sorte qu'elle peut basculer de l'un à l'autre. |
| S3 | Simple Storage Service, une interface de stockage d'objets née chez Amazon et devenue un standard de fait du secteur. Vaks PM stocke les fichiers dans un magasin compatible S3 qu'il héberge lui-même. |
| JWT | JSON Web Token, un jeton de session signé émis après une connexion réussie. Vaks PM y inscrit l'identité du locataire. |
| WS / WebSocket | Une connexion bidirectionnelle persistante entre le navigateur et le serveur, utilisée pour les mises à jour temps réel, par opposition aux appels requête/réponse ponctuels. |
| ORM | Object-Relational Mapping (mappage objet-relationnel), une bibliothèque qui associe les tables de base de données à des objets de code typés et gère les changements de schéma. |
| DCS | Distributed Configuration Store (magasin de configuration distribué) : un petit magasin clé-valeur hautement disponible qui détient l'état du cluster et élit le leader de la base de données. |
| Locataire | Une organisation isolée servie par l'instance. Chaque locataire possède sa propre base de données. |
| SMTP | Simple Mail Transfer Protocol, le protocole d'envoi d'e-mails ; utilisé pour les notifications. |
| DC | Datacenter, un site physique hébergeant un cluster. |
Pile technique
Le système est en TypeScript de bout en bout : le même langage typé couvre le front end navigateur, le back end et les contrats de données partagés entre eux, de sorte qu'une seule définition de type peut garantir que client et serveur s'accordent sur chaque charge utile. Le tableau ci-dessous liste chaque couche et la technologie qui la remplit.
| Couche | Technologie | Rôle |
|---|---|---|
| Front end | React + TypeScript, construit avec Vite ; stylé avec Tailwind | L'application web monopage servie au navigateur sous forme de ressources statiques. |
| Back end | NestJS (Node.js / TypeScript) | L'API REST et le canal WebSocket temps réel. Sans état et scalable horizontalement. |
| ORM | Prisma | Associe la base de données à du code typé et applique les changements de schéma au démarrage. |
| Base de données | PostgreSQL 17 | Le référentiel de données. Une base par locataire. Exploitée en HA avec un gestionnaire de basculement automatique. |
| Cache / file | Redis 7 | Cache, messagerie publication/abonnement et file de tâches d'arrière-plan. |
| Magasin d'objets | Compatible S3 : Garage sur Swarm, MinIO sur Kubernetes | Stocke les fichiers binaires : pièces jointes, ressources de marque, modèles de documents, archives d'audit. |
| Reverse proxy | Traefik sur Swarm ; le contrôleur Ingress du cluster sur Kubernetes | Termine le TLS et route les requêtes entrantes par nom d'hôte. |
| Orchestration | Docker Swarm (production de référence) ou Kubernetes | Planifie les conteneurs et permet les mises à jour progressives sans interruption. |
Ce qui tourne
Quel que soit l'orchestrateur utilisé, le système en cours d'exécution est le même ensemble de services. La logique applicative est maintenue sans état autant que possible : les services api, web et worker ne détiennent aucune donnée locale et peuvent être mis à l'échelle en lançant simplement plus de réplicas. Tout l'état durable réside dans les trois magasins de données (PostgreSQL, Redis et le magasin d'objets).
| Service | Rôle | Avec état ? |
|---|---|---|
web | Sert le front end monopage React (ressources statiques). | Non |
api | Back end NestJS : l'API REST (préfixe /api/v1) plus le canal WebSocket temps réel. | Non |
worker | Exécuteur de tâches d'arrière-plan : envoi d'e-mails, livraison de webhooks, transfert d'audit, tâches planifiées. Consomme la file de tâches sur Redis. | Non |
controlplane | Le point d'entrée partagé en mode multi-locataire : authentifie chaque requête et la route vers le bon locataire. Même image que api, exécutée avec un drapeau de rôle différent. | Non |
| PostgreSQL 17 | La base de données relationnelle principale. Une base par locataire. | Oui |
| Redis 7 | Cache, publication/abonnement et file de tâches d'arrière-plan. | Oui |
| Magasin d'objets (S3) | Pièces jointes, ressources de marque, modèles de documents, archives d'audit. | Oui |
| Reverse proxy | Termine le TLS et route par nom d'hôte. Traefik sur Swarm ; le contrôleur Ingress du cluster sur Kubernetes. | Non |
La couche sans état passe à l'échelle
Comme api, web et worker ne gardent rien localement, plusieurs réplicas peuvent tourner côte à côte. Ils absorbent la charge et permettent à une mise à jour progressive de se dérouler sans interruption.
L'état est concentré
Toutes les données durables résident dans exactement trois magasins. Cela maintient la surface de sauvegarde réduite et bien définie : une base de données, un cache et un magasin d'objets.
Une image, deux rôles
Le service controlplane est la même build que api avec un drapeau de rôle inversé : il n'y a donc pas de base de code distincte à maintenir pour le point d'entrée partagé.
Plan de contrôle et plan de données
L'architecture sépare deux responsabilités en deux plans logiques :
- Le plan de contrôle est le point d'entrée partagé : le reverse proxy (qui termine le TLS et route par nom d'hôte) associé à un service d'authentification partagé. Lorsqu'un utilisateur se connecte, ce service émet un
JWT(un jeton de session signé) marqué de l'identité du locataire, de sorte que chaque requête ultérieure porte déjà la preuve de l'organisation à laquelle elle appartient. - Le plan de données détient les données métier, isolées par locataire, chaque organisation dans sa propre base de données avec son propre rôle de base de données.
Une seule image de conteneur sert l'un ou l'autre rôle via un drapeau de configuration ; il n'existe pas de produit distinct pour le point d'entrée. Dans une installation on-premise mono-organisation, cette distinction est pour ainsi dire invisible, et elle ne devient significative que lorsqu'une instance sert plusieurs organisations. L'intérêt de cette séparation est le confinement. Une faille dans l'accès aux données d'un locataire (une clause WHERE oubliée, une injection SQL ou une tentative d'atteindre l'objet d'un autre utilisateur par son identifiant) reste cantonnée à la seule base de données de ce locataire, car le rôle de base de données d'un locataire ne peut lire celle d'un autre.
Modèles de location
Un locataire est une organisation isolée servie par l'instance. Un seul drapeau de configuration définit combien de locataires une instance porte et comment leurs données sont isolées. Il existe deux modes :
| Mode | Signification | Usage typique |
|---|---|---|
dedicated | Une organisation par instance. Le nom d'hôte ne sert pas à choisir un locataire, puisqu'il n'y en a qu'un. C'est le cas on-premise standard. | Une seule entreprise exploitant sa propre installation privée. |
pooled | Plusieurs organisations sur une instance. Chacune est résolue à partir de son nom d'hôte et isolée dans sa propre base, nommée <db>_<slug> (un nom de base de référence suivi de l'identifiant court de l'organisation). | Un opérateur hébergeant de nombreuses organisations sur une infrastructure partagée. |
Dans les deux modes, la règle est la même : une base de données par locataire, chacune avec son propre rôle de base de données. Les différentes bases peuvent résider dans un unique cluster PostgreSQL hautement disponible, qui constitue une infrastructure partagée pour des données séparées. L'application ne code jamais en dur la base à utiliser ; elle obtient une connexion via un contexte de locataire. En mode dedicated, ce contexte renvoie toujours la base unique ; en mode pooled, il renvoie <db>_<slug> pour le locataire résolu.
La variante en cellules
Le modèle pooled peut être déployé sous une forme plus stricte appelée cellules : au lieu d'une couche applicative partagée servant chaque locataire, chaque locataire obtient sa propre instance applicative et sa propre base de données, derrière le même plan de contrôle partagé. Cela échange un peu d'efficacité contre une isolation maximale, puisque le calcul d'un locataire n'est plus partagé avec celui de quiconque. La même image tourne toujours partout ; seule la forme du déploiement diffère. Une propriété utile de ce modèle est qu'un locataire hors production (par exemple une organisation de démo ou de test) n'est qu'une cellule de plus et est entièrement jetable : sa base peut être supprimée et réinitialisée sans toucher à aucun autre locataire.
Connexion & routage
Le reverse proxy route le trafic entrant par l'en-tête HTTP Host, le nom de domaine que le navigateur a demandé. La propriété cruciale est que le locataire est résolu avant l'authentification : le proxy transmet le Host au back end, qui en déduit l'organisation à laquelle appartient la requête, et ce n'est qu'ensuite qu'il tente de trouver l'utilisateur. Cet ordre empêche que les utilisateurs d'une organisation soient mis en correspondance avec les comptes d'une autre organisation.
Le comportement diffère entre les deux modes de location, mais l'infrastructure (le proxy, ses routeurs et les certificats TLS) est identique dans les deux. Un seul drapeau de configuration inverse le comportement ; en mode dedicated, le routage basé sur l'hôte reste simplement inactif.
| Étape | dedicated (une seule organisation) | pooled (plusieurs organisations) |
|---|---|---|
| Routage du reverse proxy | Route par chemin de requête ; le Host ne sert qu'au TLS. | Route par Host (le sous-domaine) et par chemin en secours. |
| Résolution du locataire | Le Host est ignoré ; il n'y a qu'une organisation, donc la résolution ne renvoie rien. | Le Host est mappé à une organisation (par son identifiant court ou son domaine principal). Un hôte inconnu est rejeté. |
| Recherche de l'utilisateur | Une recherche globale par adresse e-mail (une organisation, donc pas d'ambiguïté). | Une recherche restreinte à l'organisation résolue, par e-mail et organisation. |
| Résultat | Un JWT marqué de l'organisation, renvoyé avec un HTTP 200. | Un JWT marqué de l'organisation résolue, renvoyé avec un HTTP 200 ; un hôte inconnu produit un HTTP 401 (non autorisé). |
dedicated, le proxy route par chemin et le back end ignore l'hôte ; la connexion est une recherche globale par e-mail. En mode pooled, le proxy route aussi par hôte, le back end résout d'abord l'organisation, et la connexion est restreinte à cette organisation, de sorte qu'un hôte inconnu renvoie un HTTP 401.Canal temps réel
Au-delà des appels requête/réponse ordinaires, l'application maintient une connexion WebSocket persistante (un canal bidirectionnel toujours ouvert entre le navigateur et le serveur) afin que les changements tels qu'une tâche déplacée, un nouveau commentaire ou une mise à jour de statut apparaissent en direct sans rechargement de la page. Le transport est Socket.IO, une bibliothèque WebSocket largement utilisée qui ajoute la gestion de la reconnexion et du repli.
- L'authentification est requise lors de la poignée de main. La connexion doit présenter un
JWTvalide à son ouverture ; un socket non authentifié est refusé avant de pouvoir rejoindre quoi que ce soit. La connexion est donc liée à un utilisateur et un locataire connus dès son premier instant. - Les événements sont regroupés dans une room par projet. Un client ne reçoit les mises à jour que pour les projets qu'il a rejoints, plutôt qu'un flot de tout ce qui se passe sur l'instance.
- L'autorisation est appliquée à chaque événement. Le serveur applique à chaque événement temps réel les mêmes contrôles d'accès basés sur les rôles qu'il applique aux appels REST ; rejoindre la room d'un projet et recevoir ses événements requièrent tous deux les droits appropriés. Le canal en direct n'est jamais un contournement du modèle de permissions.
Réseau & ports
La surface d'exposition est délibérément réduite. Seuls les ports 80 et 443 doivent être joignables depuis les clients ; le reverse proxy redirige le HTTP en clair (80) vers HTTPS (443) et y termine le TLS. Tout autre service, y compris la base de données, le cache, le magasin d'objets et la couche applicative, communique sur le réseau overlay interne du cluster et n'est jamais publié vers l'extérieur. Les seules connexions qui quittent le cluster sont les deux connexions sortantes optionnelles (le relais SMTP et les points de terminaison de webhook ou de notification), et toutes deux proviennent du worker d'arrière-plan.
Dans la topologie HA montrée ci-dessus, deux auxiliaires se placent entre l'application et PostgreSQL : HAProxy, un répartiteur de charge qui pointe toujours vers le nœud de base de données actuellement accessible en écriture, et etcd, un DCS (magasin de configuration distribué) qui détient l'état du cluster et élit le leader de la base de données. L'application ne se connecte jamais directement à un nœud PostgreSQL. Elle passe par le répartiteur de charge, de sorte qu'un basculement lui est transparent.
Modèle à deux datacenters
Pour la résilience aux sinistres, une instance peut être répartie sur deux datacenters (DC) selon un agencement actif / passif : un DC sert tout le trafic, l'autre reste prêt à prendre le relais. Les contraintes directrices sont :
- Un orchestrateur par DC, jamais étiré. Un seul Docker Swarm ne doit pas s'étendre sur les deux sites : son quorum de consensus est fragile sur un lien longue distance et risque le split-brain (deux moitiés se croyant chacune leader). Chaque DC exécute donc son propre cluster indépendant, avec son propre quorum local.
- Un seul PostgreSQL accessible en écriture. PostgreSQL n'accepte les écritures que sur un seul nœud. Le DC principal est en lecture/écriture ; le DC de secours exécute un standby cluster Patroni alimenté par une réplication asynchrone, de sorte que les écritures sont copiées avec un léger délai.
- Un seul magasin d'objets sur les deux sites, en zones. Le magasin compatible S3 s'étend sur les deux DC comme un cluster unique, en utilisant une zone par site afin que chaque objet soit répliqué géographiquement.
- Du GeoDNS en frontal. Un service DNS géographique (ou un répartiteur de charge global) se place devant les deux plans de contrôle et oriente les visiteurs vers le DC actif.
Comme la réplication est asynchrone, un basculement (promotion du secours) comporte un RPO (Recovery Point Objective, la perte de données maximale tolérée) non nul : les quelques secondes d'écritures pas encore répliquées peuvent être perdues. La couche applicative sans état, en revanche, tourne de façon identique dans les deux DC et ne pose aucune contrainte de ce genre. Deux sites véritablement actifs servant des écritures en même temps sont hors périmètre, car PostgreSQL n'admet qu'un seul rédacteur ; cela exigerait de géo-distribuer les locataires de sorte que chacun soit principal dans un seul DC.