Architecture technique

Vaks PM · Architecture technique · juin 2026

Objet de ce guide. Ce document décrit l'architecture de Vaks PM : de quoi le système est constitué, comment ses parties s'assemblent et les principes qui le façonnent. Il s'adresse à un administrateur système qui découvre le produit et reste à un niveau conceptuel. Il explique comment les pièces s'articulent, pas les commandes exactes pour les installer. Pour les procédures d'installation pas à pas, voir les guides de déploiement liés en bas de page. Chaque acronyme est défini à sa première occurrence et rassemblé dans le glossaire ci-dessous.

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 :

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.

TermeSignification
On-premiseLogiciel hébergé sur une infrastructure que l'organisation contrôle, plutôt que sur un service en ligne exploité par un fournisseur.
HAHigh Availability (haute disponibilité) : une topologie qui survit à la perte d'un hôte unique sans interruption.
DNSDomain Name System (système de noms de domaine), qui associe des noms d'hôtes à des adresses réseau.
TLSTransport Layer Security, la couche de chiffrement derrière HTTPS.
VIPVirtual 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.
S3Simple 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.
JWTJSON Web Token, un jeton de session signé émis après une connexion réussie. Vaks PM y inscrit l'identité du locataire.
WS / WebSocketUne 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.
ORMObject-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.
DCSDistributed 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.
LocataireUne organisation isolée servie par l'instance. Chaque locataire possède sa propre base de données.
SMTPSimple Mail Transfer Protocol, le protocole d'envoi d'e-mails ; utilisé pour les notifications.
DCDatacenter, 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.

CoucheTechnologieRôle
Front endReact + TypeScript, construit avec Vite ; stylé avec TailwindL'application web monopage servie au navigateur sous forme de ressources statiques.
Back endNestJS (Node.js / TypeScript)L'API REST et le canal WebSocket temps réel. Sans état et scalable horizontalement.
ORMPrismaAssocie la base de données à du code typé et applique les changements de schéma au démarrage.
Base de donnéesPostgreSQL 17Le référentiel de données. Une base par locataire. Exploitée en HA avec un gestionnaire de basculement automatique.
Cache / fileRedis 7Cache, messagerie publication/abonnement et file de tâches d'arrière-plan.
Magasin d'objetsCompatible S3 : Garage sur Swarm, MinIO sur KubernetesStocke les fichiers binaires : pièces jointes, ressources de marque, modèles de documents, archives d'audit.
Reverse proxyTraefik sur Swarm ; le contrôleur Ingress du cluster sur KubernetesTermine le TLS et route les requêtes entrantes par nom d'hôte.
OrchestrationDocker Swarm (production de référence) ou KubernetesPlanifie les conteneurs et permet les mises à jour progressives sans interruption.
Le choix entre Docker Swarm et Kubernetes ne change rien à l'application : les deux exécutent les images identiques et prennent en charge le même stockage d'objets on-premise. La décision est dictée par l'outillage existant de l'équipe d'exploitation, pas par une différence de fonctionnalité. Voir Déploiement Docker Swarm et Déploiement Kubernetes.

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

ServiceRôleAvec état ?
webSert le front end monopage React (ressources statiques).Non
apiBack end NestJS : l'API REST (préfixe /api/v1) plus le canal WebSocket temps réel.Non
workerExé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
controlplaneLe 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 17La base de données relationnelle principale. Une base par locataire.Oui
Redis 7Cache, 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 proxyTermine 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 :

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.

Navigateurs un nom d'hôte par organisation 443 · 80 PLAN DE CONTRÔLE · PARTAGÉ Reverse proxy TLS · routage par nom d'hôte Authentification partagée émet JWT { tenant } JWT { tenant } PLAN DE DONNÉES · ISOLÉ PAR ORGANISATION Organisation A api · worker base db_orgA Organisation B api · worker base db_orgB Organisation C api · worker base db_orgC
Architecture logique : un plan de contrôle partagé authentifie et route par nom d'hôte, tandis que les données de chaque organisation sont isolées dans sa propre base. Seul le port HTTPS public est exposé aux clients.

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 :

ModeSignificationUsage typique
dedicatedUne 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.
pooledPlusieurs 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.

utilisateur @ acme utilisateur @ globex utilisateur @ demo PLAN DE CONTRÔLE · partagé (connexion + routage) Auth partagée + annuaire de locataires → JWT { tenant } Reverse proxy · TLS *.example.com routage par Host → cellule du locataire acme → globex → demo → PLAN DE DONNÉES · isolé par locataire Cellule ACME api-acme (app) db-acme Cellule GLOBEX api-globex (app) db-globex Cellule DEMO jetable · supprimer + réinitialiser api-demo (app) db-demo db-acme · db-globex · db-demo = N bases + rôles distincts dans 1 cluster PostgreSQL HA (infrastructure partagée) Pooled : 1 app partagée sert tous les locataires → choisit db-<tenant> · Cellules : 1 app + 1 db par locataire (isolation max) même image partout · seul commutateur : mode de location = dedicated | pooled
La variante en cellules du multi-locataire : chaque locataire possède sa propre instance applicative et sa propre base derrière un plan de contrôle partagé. Les locataires hors production (comme une démo) sont des cellules jetables qui peuvent être supprimées et réinitialisées de façon isolée.

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.

Étapededicated (une seule organisation)pooled (plusieurs organisations)
Routage du reverse proxyRoute 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 locataireLe 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'utilisateurUne 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ésultatUn 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é).
MODE dedicated · on-premise (1 org) MODE pooled · multi-locataire (N orgs) Navigateur Host: pm.company.com Host REVERSE PROXY routeur : PathPrefix(/api) routage par CHEMIN · Host = TLS uniquement API · connexion résoudre le locataire depuis Host mode=dedicated → aucun (le Host est ignoré) connexion (locataire = aucun) trouver l'utilisateur par { email } JWT { orgId } → 200 OK résolu par e-mail (1 org en production) Navigateur · 2 locataires acme… / globex.example.com Host REVERSE PROXY · TLS *.example.com Host(<sub>.example.com) → routage par HOST + secours PathPrefix(/api) API · connexion résoudre le locataire depuis Host → org acme→org acme · globex→org globex Host inconnu / apex → 401 connexion (locataire = org.id) trouver l'utilisateur par { email, organizationId } JWT { orgId } → 200 OK acme→orgId acme · globex→orgId globex PostgreSQL HA (via répartiteur de charge) users users
Flux de connexion dans les deux modes de location. En mode 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.

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.

Clients navigateur · consommateur d'API 443 · 80 Traefik reverse proxy · TLS 3000 web · api · worker sans état · scalés horizontalement 6379 5000 3900 Redis 7 cache · file (BullMQ) HAProxy répartiteur de charge PostgreSQL PostgreSQL 17 cluster HA Patroni etcd élection de leader (DCS) Garage (S3) stockage d'objets 5432 2379 · 2380 SORTANT DEPUIS LE WORKER · OPTIONNEL Relais SMTP 587 · 465 (TLS) Points de terminaison Webhook / Teams 443 (HTTPS)
Flux réseau et ports de service par défaut (topologie haute disponibilité). Seuls les ports 443 et 80 sont exposés aux clients ; tout autre port reste sur le réseau overlay interne. Sur Kubernetes, les mêmes flux se traduisent en Services et en contrôleur Ingress. Les numéros de port sont des valeurs par défaut et peuvent être modifiés par configuration.

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

Préparé, non validé. Le modèle à deux datacenters ci-dessous est un cas limite. L'outillage peut en générer la configuration, mais celle-ci n'a pas été validée de bout en bout contre deux clusters réels, et les valeurs réseau (latence inter-site, interconnexion, DNS géographique) doivent être adaptées à chaque site. Un déploiement standard n'a besoin de rien de tout cela ; voir Déploiement Docker Swarm.

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 :

GeoDNS / LB global │ ┌─────────┴─────────┐ ▼ ▼ ┌── DC1 (principal) ─┐ ┌── DC2 (secours) ──┐ │ Swarm A │ │ Swarm B │ │ controlplane+api │ │ controlplane+api │ (chaud) │ Postgres R/W │→→│ Postgres standby │ async │ Garage (zone dc1) │↔↔│ Garage (zone dc2) │ réplication S3 └───────────────────┘ └───────────────────┘

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.


À lire aussi : Le produit · Déploiement Docker Swarm · Déploiement Kubernetes · Sécurité · Exploitation · Démo