Tous les articles

Tous les articles

Premier jour : un framework multi-tenant à partir d'un repo vide

La première soirée de la plateforme : un plan écrit, un outillage qui refusait le code bâclé, et le contexte de requête sur lequel reposerait le multi-tenant.

Le repo est passé de vide à sept commits le soir du 1er février 2026, entre dix heures vingt et minuit moins dix. Aucun d'eux ne répondait à une requête. Ils contenaient un plan, un outillage, un objet de configuration, un contexte de requête, des traductions et une norme de documentation, et deux de ces éléments, le plan et le contexte, façonnent encore la plateforme.

Le plan est venu avant le code#

Le premier commit contient plus de planification que de code : un document d'architecture, un plan d'implémentation en dix phases et vingt-deux fichiers de tâches, un par chantier, du squelette du repo jusqu'à un site de documentation. Le code se résume à un package vide et à un test factice.

Le plan décrit un framework backend opinionated plutôt qu'une application, parce que l'application n'existait pas encore. Ce qu'on savait, c'était le type de logiciel qu'il porterait : des applications métier où plusieurs organisations partagent un même déploiement, où ce que chacun peut voir est décidé par des règles plutôt que par chaque écran, et où les fichiers, les journaux d'audit et les traductions sont des propriétés de la base plutôt que des fonctionnalités à ajouter. Tout ça est entré dans le plan comme le travail du framework, pour que chaque modèle écrit par la suite en hérite sans rien demander.

Sa décision centrale était le multi-tenant par séparation logique dans une seule base de données. Chaque requête porterait l'organisation pour laquelle elle agit, chaque table appartenant à un tenant porterait une colonne d'organisation ajoutée par le modèle de base, et la session de base de données ajouterait d'elle-même le filtre d'organisation à chaque requête. Un deuxième article raconte comment ça s'est passé. Le reste du plan constituait le socle opérationnel : des règles d'accès définies dans le code, des journaux d'audit, la suppression logique, des limites de débit par tenant, des feature flags, des health checks, et des migrations uniquement additives pour qu'un déploiement blue/green ne tombe jamais sur une colonne que l'ancienne version lit encore.

Le plan d'implémentation s'adressait à un agent de code. Je prenais les décisions et je relisais le résultat ; le plan était le moyen de transmettre ces décisions intactes à l'agent, un fichier de tâche à la fois.

Ce que la première soirée a construit#

L'outillage est arrivé dans la minute qui a suivi le squelette : pre-commit avec Ruff, codespell et mypy, pour que rien qui échoue à ces contrôles ne puisse être commité dès le départ. Les dépendances nommaient la stack choisie par le plan : FastAPI, SQLAlchemy 2.0 en asynchrone, Pydantic, PostgreSQL, Redis, et arq pour les tâches de fond.

La configuration est allée dans une classe de settings Pydantic lue depuis l'environnement : la base de données, Redis et les paramètres de sécurité, typés et testés.

Le contexte de requête était la pièce la plus importante. C'est un petit module de variables de contexte qui contient l'organisation courante, l'utilisateur courant et la session de base de données, définies au début d'une requête et réinitialisées à la fin. Les variables de contexte suivent chaque tâche asyncio, si bien que deux requêtes traitées en même temps par un même worker voient chacune leur propre organisation. Le premier test du contexte vérifiait exactement ça : il lançait deux tâches en parallèle avec deux organisations différentes et contrôlait que chacune relisait bien la sienne.

Les traductions sont venues ensuite, avec Babel : un middleware qui lit Accept-Language et redescend d'une variante régionale comme nl-BE vers sa langue, et un wrapper gettext pour les messages. L'anglais, le français et le néerlandais ont été mis en place, et les erreurs de validation ont reçu une traduction française dès le premier soir, avant qu'il y ait grand-chose d'autre à traduire.

Les trois derniers commits de la soirée ont appliqué des docstrings au format Google au code et aux tests, puis les règles de style écrites à chaque fichier restant. Il n'y avait qu'une poignée de fichiers, et c'est précisément le moment où une norme coûte peu à adopter.

Ce qui a survécu et ce qui n'a pas tenu#

En septembre 2026, l'essentiel de la stack de ce soir-là est toujours en place : FastAPI, SQLAlchemy asynchrone, Pydantic, PostgreSQL, Redis, arq et Babel. Le contexte de requête est le même module, qui a grandi. À côté de l'organisation et de l'utilisateur, il porte désormais les groupes, privilèges et droits d'accès effectifs que lit la couche de sécurité, et les tâches de fond le rejouent pour qu'une tâche s'exécute avec l'identité de la requête qui l'a mise en file.

Plusieurs choix ont été revus, certains en quelques jours :

  • mypy a cédé la place à basedpyright quatre jours plus tard, pour une meilleure inférence sur le système de champs.
  • Les classes de politiques en code, le modèle de sécurité du plan, sont devenues un DSL de règles déclaratives au bout de dix jours : des règles déclarées sur le modèle et combinées par des opérateurs logiques, qu'un seul moteur traduit à la fois en filtre de requête et en contrôle sur un enregistrement.
  • L'organisation a été renommée tenant le quatrième jour, puis est redevenue organisation en avril, quand le multi-tenant a été reconstruit autour des organisations et d'appartenances many-to-many.
  • Poetry a cédé la place à uv en avril.
  • Google Cloud Storage, le stockage de fichiers du plan, a cédé la place à un backend compatible S3 en juillet, dès lors que l'infrastructure allait tourner chez Scaleway.
  • Le néerlandais n'est pas resté : l'application parle anglais et français.

La limitation de débit, la suppression logique et les health checks ont été construits comme prévu.

Ce que ça a coûté#

Une soirée, et une habitude : le deuxième commit a placé un contrôle devant chaque commit qui a suivi. Les contrôles eux-mêmes ont changé. Pre-commit a cédé la place à prek deux semaines plus tard, mypy à basedpyright, et les vérifications se sont ajoutées depuis plutôt qu'assouplies, jusqu'à des règles sur la prose des commentaires et des pages comme celle-ci.