Project

General

Profile

Actions

Feature #1429

open
CD

Spike architecture : renforcer et faire évoluer le modèle multi-tenant

Feature #1429: Spike architecture : renforcer et faire évoluer le modèle multi-tenant

Added by Client Dashboard 15 days ago. Updated 4 days ago.

Status:
Submitted
Priority:
Normal
Assignee:
-
Start date:
07/20/2026
Due date:
% Done:

0%

Estimated time:
spec_ref:
build_status:
build_number:
ci_run_url:
scan_status:
scan_report_url:
deploy_status:
preprod_url:
deployed_at:
branch:
pr_url:
security_key:
severity:
paused:
Yes

Description

> **Idée / spike de cadrage — ce ticket n'est PAS une spécification.** Il capture une intention produit et recense les questions à trancher. Aucune des pistes ci-dessous n'est arbitrée, et rien ici ne demande de « réécrire l'application ».

## Objectif

L'application est **déjà multi-tenant au niveau applicatif** : les données sont cloisonnées par client, les projets appartiennent à un client, et les crédits sont tenus par client. La question posée n'est donc pas « comment devenir multi-tenant », mais :

- **quelles garanties d'isolation** manquent aujourd'hui pour que le cloisonnement soit *démontré* et non seulement *implémenté* ;
- **quelles capacités multi-tenant** (onboarding autonome, personnalisation, isolation au déploiement) valent la peine d'être construites, et dans quel ordre.

Ce ticket sert de support d'arbitrage : cinq axes indépendants, chacun avec ses contraintes connues et ses questions ouvertes.

## Contexte — le modèle de tenancy tel qu'il existe aujourd'hui

**Le client est à la fois le tenant et le compte utilisateur.** Une même entité porte l'identité de connexion (adresse e-mail, empreinte de mot de passe — optionnelle pour un compte provisionné par SSO), la langue, la société et l'indicateur administrateur. Il n'existe **pas de séparation entre « organisation » et « utilisateur »** : un tenant équivaut aujourd'hui à un compte de connexion. La société n'est qu'un champ de profil libre.

**Les projets appartiennent à un client mais sont partagés par appartenance.** Chaque projet porte son client propriétaire, sa clé de projet et son rattachement Redmine. L'accès n'est cependant plus décidé sur la seule propriété : une table d'appartenance permet d'associer plusieurs clients à un même projet, le propriétaire d'origine étant systématiquement membre. **Cette appartenance est binaire — il n'y a aucun rôle** : on est membre ou on ne l'est pas. Le seul axe de privilège existant est l'indicateur administrateur global.

**Le cloisonnement est appliqué dans la couche API, à l'entrée de la requête.** Une requête est rattachée à son tenant par un cookie de session signé, qui est résolu en client authentifié — le compte étant systématiquement rechargé depuis la base à chaque requête plutôt que déduit du jeton. Deux gardes complémentaires s'y greffent : l'une réserve les routes d'administration aux comptes administrateurs, l'autre autorise l'accès à un projet **uniquement si le client appelant en est membre**. Ces gardes constituent aujourd'hui le point de passage du cloisonnement, et elles vivent **dans la couche de présentation**.

**Ce point de passage relève toutefois de la convention, pas de la construction.** La garde projet est déclarée route par route : elle est appliquée avec constance sur l'ensemble des routes existantes, mais **rien n'empêche structurellement une future route de l'omettre**. Par ailleurs, l'autorisation n'est pas uniforme : **trois idiomes distincts coexistent** — la garde d'appartenance projet, une règle d'isolation des conversations écrite en ligne dans les routes, et une règle « soi-même ou administrateur » propre aux lectures de facturation — avec des codes de refus différents selon les cas.

**Il n'y a aucune application du cloisonnement au niveau de la base.** Le schéma est partagé par tous les tenants, sans *row-level security* PostgreSQL ni schéma par tenant : aucune politique d'isolation n'est définie côté base. Les dépôts de données reçoivent l'identifiant de tenant **en simple argument fourni par l'appelant** et n'ont aucune connaissance de l'utilisateur connecté. Le filtrage repose donc **entièrement sur la discipline du code applicatif** : une requête qui oublierait le filtre ne serait arrêtée par rien.

**Deux chemins s'exécutent hors de toute requête authentifiée.** Le worker d'indexation tourne en processus séparé et n'est cadré que par l'identifiant de projet porté par le job qu'il traite. Côté assistant, la recherche sémantique est filtrée **par projet uniquement** : ce filtre est le seul rempart empêchant la connaissance indexée d'un projet d'alimenter la réponse donnée dans un autre.

**Les crédits sont tenus par client, pas par projet.** Le solde est rattaché au client — un solde par compte — et suit un modèle à deux réserves indépendantes (maintenance et évolutions), chacune finie ou illimitée, adossées à une formule d'abonnement. Chaque mouvement (changement de formule, ajustement manuel, débit au dépôt d'un ticket, remboursement à sa suppression) est tracé dans un historique immuable. **Il en résulte un décalage d'axes** : l'accès est cadré par *projet*, la facturation par *client*. Sur un projet partagé, la consommation est imputée au membre qui agit, sans réserve commune au projet ni à l'organisation.

**Toute la gestion commerciale est réservée à l'administration.** Création de compte, changement de formule, ajustement de crédits et rattachement d'un projet à un client passent exclusivement par des routes d'administration protégées. Un client ne peut pas non plus inviter un autre client sur un de ses projets.

**Architecture et déploiement.** Le backend est un **monolithe hexagonal** (domaine / application / infrastructure / présentation) déployé comme un composant unique, aux côtés du frontend Next.js — soit deux composants déployés. Le worker d'indexation n'est pas un composant déployé à part entière : il tourne à partir de la même image que l'API, en service supplémentaire, sur la même base. Enfin, l'**agrégateur** — source unique des données transverses de santé projet — est **déjà un service séparé**, appelé en HTTP.

**Identité.** L'authentification par mot de passe cohabite avec le SSO Microsoft Entra ID (cf. #1311), un client s'authentifiant par ce biais. Les deux chemins débouchent sur le même cookie de session, indistinguables pour le reste de l'API. La configuration Entra est en revanche **globale au déploiement** : un seul annuaire pour l'ensemble des tenants, avec une liste d'autorisation explicite dont la valeur vide interdit tout accès.

## 1. Renforcement de l'isolation

**Contrainte connue.** Le cloisonnement est correct mais **structurellement fragile** : il est appliqué à l'entrée de l'API et n'est garanti par rien en dessous. Il n'existe **aucune défense en profondeur** — une garde omise sur une future route, un accès hors du chemin nominal ou une requête mal cadrée constituerait une lecture inter-tenant complète, sans filet pour la rattraper.

**Questions ouvertes.**

- Faut-il porter l'isolation **au niveau de la base** — *row-level security* PostgreSQL sur les tables portant un tenant, ou schéma par tenant ? Quel coût en migrations, en performance et en complexité de connexion (propagation du tenant courant à la session SQL) ?
- Peut-on réduire le cloisonnement à un **point de passage unique et non contournable** côté accès aux données, plutôt qu'à une garde déclarée route par route ? Et **unifier les trois idiomes d'autorisation** actuels en une règle unique, avec une sémantique de refus cohérente ?
- Comment cadrer les **chemins hors requête** — l'indexation et la recherche sémantique — qui ne bénéficient d'aucune garde et reposent sur un simple filtre par projet ?
- Faut-il **réconcilier les axes** d'accès (projet) et de facturation (client), ou introduire une notion d'organisation qui porte les deux ?
- Quels **tests prouvent activement** qu'une lecture inter-tenant échoue ? Aujourd'hui l'absence de fuite est constatée par construction ; il faudrait des tests qui **tentent explicitement l'accès croisé** sur chaque ressource et exigent un refus, ainsi qu'un test qui vérifie qu'aucune route cadrée par projet n'échappe à la garde.

**Lien direct avec le BYO-IA (#1428).** Exposer un serveur MCP ou un accès Redmine direct au client **contournerait entièrement l'autorisation applicative actuelle**, puisque celle-ci vit dans la couche API du dashboard. La frontière d'isolation devrait alors être **rétablie sur cette nouvelle surface**. Un cloisonnement garanti plus bas dans la pile (base ou couche d'accès aux données) serait **réutilisable par toute surface future**, là où une garde par route ne l'est pas. **Le renforcement de l'isolation est donc défendable comme prérequis de #1428**, et non comme un chantier parallèle.

## 2. Onboarding self-service

**Contrainte connue.** Le provisionnement d'un client est aujourd'hui **entièrement manuel** : création du compte, attribution d'une formule, ajustement des crédits et rattachement des projets sont tous des actions d'administration. Il n'existe **aucun parcours d'achat ni de rechargement autonome**, et aucune intégration de paiement. Autrement dit, **chaque événement commercial exige une intervention humaine**.

**Questions ouvertes.**

- Que doit pouvoir faire un client **sans assistance** : créer son compte, être provisionné, rattacher ses projets, changer de formule, recharger ses crédits ?
- Le rattachement d'un projet peut-il être self-service, sachant qu'il suppose une correspondance avec un projet Redmine existant et une clé de projet connue de l'agrégateur ? Quelle validation, quelle vérification de légitimité ?
- Quel modèle de paiement et de facturation (souscription, rechargement à l'unité, prélèvement récurrent) ? Quelles obligations comptables et quel cycle de renouvellement des réserves ?
- Faut-il introduire une notion d'**organisation distincte du compte utilisateur**, avec plusieurs utilisateurs et des **rôles** ? Le modèle actuel confond tenant et compte, et l'appartenance projet est sans rôle : en l'état, **aucun onboarding d'équipe n'est possible**, et un client ne peut pas inviter un collaborateur lui-même.
- Quel niveau de contrôle conserver : validation manuelle a posteriori, ou autonomie complète ?

**C'est l'infrastructure commerciale manquante.** Tant qu'elle n'existe pas, la croissance du nombre de clients reste bornée par le temps d'administration disponible.

## 3. White-label / configuration par tenant

**Contrainte connue.** Il n'existe aujourd'hui **aucune personnalisation par tenant** : identité visuelle, domaine, configuration des modèles et paramètres fonctionnels sont globaux au déploiement. La **langue du client est la seule préférence réellement par compte** ; il n'existe ni système de drapeaux de fonctionnalités, ni stockage de configuration par client.

**Questions ouvertes.**

- **Identité visuelle par tenant** : logo, palette, dénomination. Jusqu'où va la personnalisation — habillage léger ou marque blanche complète ?
- **Domaine personnalisé** par client : quelles implications sur les certificats, le routage, les cookies de session et la résolution du tenant à partir du domaine ?
- **Fournisseur d'identité par tenant.** C'est le point le plus concret et le plus engagé : un client s'authentifie déjà via Entra ID (#1311) alors que la configuration est **globale au déploiement**, avec un unique annuaire pour tous. **La couche d'authentification doit donc devenir enfichable par tenant** — plusieurs annuaires, plusieurs configurations SSO, cohabitant avec l'authentification par mot de passe. Question centrale : **comment résout-on le tenant *avant* d'avoir authentifié l'utilisateur** (découverte par domaine de messagerie, par sous-domaine, autre) ?
- **Drapeaux de fonctionnalités par tenant** : activer/désactiver des modules par client. Où vit cette configuration, et comment éviter qu'elle ne devienne un facteur de complexité diffus dans tout le code ?
- **Configuration LLM / modèle par tenant** : modèle, fournisseur, voire clés propres au client — piste qui recoupe directement #1428.

## 4. Isolation au déploiement

**Contrainte connue.** L'infrastructure est aujourd'hui **entièrement mutualisée** : une base, un schéma, une instance applicative, un worker et un agrégateur pour l'ensemble des tenants.

**Questions ouvertes.**

- Faut-il pouvoir proposer une **base dédiée**, voire une **instance dédiée**, à un client ayant des exigences de **résidence des données** ou d'**isolation contractuelle** ?
- Quelle granularité : base dédiée sur infrastructure partagée, ou déploiement complet dédié ?
- Comment le provisionnement, les migrations, la supervision et les livraisons s'adaptent-ils à un parc d'instances plutôt qu'à une seule ?
- Quelle articulation avec l'axe 1 : une isolation forte au niveau de la base (schéma par tenant) constitue-t-elle un palier intermédiaire suffisant pour répondre à la plupart de ces exigences sans aller jusqu'au déploiement dédié ?

**Arbitrage explicite.** Un déploiement par tenant **multiplie la surface opérationnelle** — migrations à jouer *n* fois, supervision à démultiplier, livraisons à coordonner sur un parc qui finit par diverger en versions. Ce coût est **permanent et croît linéairement** avec le nombre de clients. Cet axe ne doit donc être ouvert **qu'à la demande**, sur exigence réelle et contractualisée, et **jamais de façon spéculative**.

## 5. Étude : découpage en microservices

Le product owner souhaite que cette piste soit étudiée. Elle est présentée ici **honnêtement et sans présupposé** : l'étude peut parfaitement conclure qu'il ne faut rien découper.

**État des lieux.** Le backend est un **monolithe hexagonal** — domaine, application, infrastructure, présentation — déployé en un composant unique aux côtés du frontend. Deux constats doivent cadrer le débat :

- **La structure hexagonale procure déjà l'essentiel du bénéfice de modularité** recherché dans un découpage : frontières explicites entre domaines, dépendances inversées, adaptateurs substituables — **sans le coût de la distribution** (latence réseau, cohérence éventuelle, transactions réparties, observabilité distribuée, complexité de déploiement).
- **Le système n'est pas un monolithe pur.** L'agrégateur est **déjà un service séparé**, et c'est précisément le découpage qui se justifiait : il agrège des sources externes, avec un cycle de vie et un profil de défaillance propres. Le worker tourne par ailleurs déjà en processus distinct, bien qu'à partir de la même image et sur la même base.

**Les vraies questions à instruire.**

- **Existe-t-il un problème démontré à résoudre ?** Un découpage se justifie par une contrainte réelle : un goulot de **montée en charge** localisé, un besoin de **déployer indépendamment**, ou une **frontière d'équipe** à matérialiser. **Aucun de ces trois éléments n'est aujourd'hui constaté**, et il n'existe pas de mesure de charge permettant d'objectiver le premier. Produire cette mesure devrait précéder toute décision.
- **La taille de l'équipe.** L'équipe est actuellement **très réduite**. C'est l'argument classique — et le plus solide — contre les microservices : le découpage transforme des appels de fonction en contrats réseau à versionner et à maintenir, et fait payer un coût de coordination qui n'a de sens qu'à partir de plusieurs équipes autonomes.
- **Quelles coutures seraient seulement candidates ?** Deux chemins se distinguent du reste, parce qu'ils ont des **profils de charge et de défaillance différents du CRUD** :
- le **chemin worker / indexation** — traitement par lots, tolérant à la latence, consommateur de ressources par à-coups, **déjà exécuté en processus séparé** ;
- le **chemin assistant / LLM** — appels externes longs, en flux, avec un taux d'échec propre, et dont la dégradation ne devrait pas affecter les opérations courantes.

Le reste du domaine (identité, projets, tickets, facturation) est fortement cohérent et transactionnel : l'y découper importerait des transactions réparties sans contrepartie.
- **Quelles alternatives moins coûteuses** répondraient au même besoin — mise à l'échelle horizontale du monolithe, isolation par files d'attente, limitation de débit, séparation des chemins de lecture et d'écriture ?

**Conclusion proposée pour l'étude.** Le découpage **exige un problème démontré avant d'être engagé**. Si un tel problème apparaît, le premier pas doit être **l'extraction d'une seule couture — le worker d'indexation, candidat le plus naturel puisqu'il tourne déjà séparément** — en étape **réversible** et mesurable, et non un découpage d'ensemble.

## Priorisation

Ces cinq axes sont **indépendants** et doivent être **séquencés, pas menés de front**. Les attaquer ensemble reviendrait à ouvrir cinq chantiers structurants simultanés sans arbitrage, avec un risque élevé de n'en terminer aucun.

Deux repères pour la séquence :

- **Le renforcement de l'isolation (1) est le seul axe porteur d'un déclencheur concret à court terme** : #1428 en dépend, puisqu'exposer une nouvelle surface d'accès contournerait l'autorisation actuelle. C'est le candidat naturel au premier cadrage, et le seul dont le report a un coût identifié.
- **Le découpage en microservices (5) est une étude sans problème démontré derrière elle.** Il doit rester au stade de l'instruction et ne mobiliser aucun développement tant qu'une contrainte réelle n'est pas observée et mesurée.

Les axes 2, 3 et 4 sont pilotés par la demande : l'onboarding self-service (2) par l'ambition commerciale, la personnalisation par tenant (3) par les besoins clients — son volet fournisseur d'identité étant déjà partiellement engagé par #1311 —, et l'isolation au déploiement (4) uniquement sur exigence contractuelle avérée.

## Prochaine étape

Cadrage par axe, indépendamment. Chaque axe doit faire l'objet de sa propre phase de conception, avec ses arbitrages, son périmètre et son estimation. **Aucun développement avant arbitrage** sur les axes retenus et sur leur ordre de traitement.

## Classification
- feature

Actions

Also available in: PDF Atom