Conçu par des ingénieurs, pour des ingénieurs.
L'architecture, la sécurité et l'exploitation, détaillées à l'intention des personnes qui intégreront cette plateforme, y bâtiront des modules et valideront la revue d'architecture. Rien ici n'est édulcoré pour le marketing.
dotnet build CleverInit.slnx
0 Warning(s) · 0 Error(s)
dotnet test CleverInit.slnx
4,028 passed · 0 failed
- 4 028
- Tests automatisés, host-solution
- 0
- Avertissements de build tolérés
- 5
- Behaviors sur chaque command
- 5
- Étapes pour résoudre un tenant
Chaque décision d'architecture est délibérée. Rien n'y figure par hasard.
Dix décisions qui ont façonné tout le reste.
Chacune est imposée par le code ou vérifiée dans la CI — pas consignée dans un wiki en espérant que ça tienne.
- 01
Une Clean Architecture que le compilateur impose
Domain ← Application ← Infrastructure ← API sont des références de projet, pas des conventions. Un handler qui va puiser dans l'infrastructure fait échouer le build.
- 02
Un seul pipeline pour chaque requête
Logging, tracing, validation, invalidation de cache, transaction — le même ordre à chaque fois, pour que le timing, la validation et la sémantique des erreurs restent uniformes d'un endpoint à l'autre.
- 03
Une base de données par client inscrit
Un compte créé via l'inscription reçoit sa propre base de données SQL, et sa connection string est chiffrée par ASP.NET Core Data Protection avant d'être stockée.
- 04
Les lectures sont des projections avec un contrat de pagination
Chaque lecture est une projection no-tracking vers un DTO, et chaque endpoint de liste renvoie un résultat paginé avec un plafond strict. Un jeu de résultats sans limite n'est pas une option que le code propose.
- 05
Pas de SQL brut, nulle part
Uniquement des requêtes EF Core paramétrées. Les entrées utilisateur ne sont jamais interpolées dans du SQL, parce que le chemin qui construirait cette chaîne n'existe tout simplement pas.
- 06
Des events qui survivent à un crash — avec une sémantique honnête
Ce qui doit être annoncé est stocké dans la base de données, à côté du travail lui-même, puis publié par un seul sweeper. La livraison a lieu au moins une fois, le nombre de retries est fini, et les dead-letters sont annoncées, pas dissimulées.
- 07
Les migrations sont append-only
Une migration livrée n'est jamais modifiée. Chaque changement de schéma est une nouvelle migration vers l'avant, dans le même commit que le changement d'entité qui l'exige.
- 08
Les API sont versionnées
La surface HTTP repose sur Asp.Versioning, si bien qu'un breaking change est une nouvelle version explicite plutôt qu'une modification silencieuse sous votre intégration.
- 09
Les erreurs sont en RFC 7807, en langage clair
La validation devient un 400 avec une carte d'erreurs par champ, l'introuvable un 404, les doublons un 409, les règles métier un 422 — et le detail est toujours une phrase complète, jamais un nom de classe ni un fragment de SQL.
- 10
Les modules doivent prouver qu'ils se déchargent
Chargez un module, libérez chaque référence, forcez un garbage collection, vérifiez que la référence faible vers son load context est morte. Si un module ne peut pas réellement se décharger, ce contrôle échoue.
Quatre couches, une seule direction.
Les flèches de dépendance ci-dessous sont des références de projet. Les franchir ne produit pas un commentaire de revue de code — cela fait échouer le build.
- Domain La plus interne · zéro dépendance
- Entités, value objects, domain events, domain exceptions, constantes.
- Application Cas d'usage
- Dépend uniquement de Domain. Commands, queries, handlers, validators, mappers, pipeline behaviors.
- Infrastructure Adaptateurs
- Dépend d'Application et de Domain. EF Core, Redis, MassTransit, JWT, BCrypt, repositories.
- API Composition root
- Dépend des trois autres. Controllers, middleware, dependency injection, OpenAPI, mapping des problem-details.
Les couches internes ne référencent jamais vers l'extérieur. La direction est imposée là où elle est incontestable : dans les fichiers de projet.
Diagramme : quatre anneaux concentriques — API tout à l'extérieur, puis Infrastructure, puis Application, avec Domain au cœur. Les références de dépendance ne pointent que vers l'intérieur ; une référence qui pointe vers l'extérieur fait échouer le build.
Le pipeline de requêtes.
Chaque command traverse les mêmes cinq behaviors, du plus externe au plus interne. Les queries traversent l'étape de transaction sans y toucher.
01
Logging
Nom du command et millisecondes écoulées. Les payloads ne sont jamais journalisés.
02
Tracing
Le cycle de vie complet du handler, enveloppé dans un span OpenTelemetry.
03
Validation
Chaque validator enregistré s'exécute ; un échec devient un 400 avec une carte d'erreurs par champ.
04
Invalidation de cache
Clés de cache marquées comme obsolètes après un command réussi.
05
Transaction
Les commands sont enveloppés dans une transaction EF Core. Les queries sautent cette étape.
Un client, une base de données.
Un compte créé via l'inscription reçoit sa propre base de données SQL, provisionnée et migrée au moment même où le compte est créé.
La connection string de cette base de données est chiffrée via ASP.NET Core Data Protection avant d'être stockée.
Emplacement d'image — visuel à venir
Diagramme : comment les données d'un seul compte créé via l'inscription restent séparées — sa propre base de données, avec les schémas des modules à l'intérieur.
Déterminer à quel client appartient une requête forme une chaîne de cinq étapes. Elle s'arrête à la première correspondance.
- 1Le claim signé du tokenLe token porte notre signature ; celle-ci est vérifiée avant que le claim qu'il contient soit tenu pour fiable.
- 2L'en-tête X-Tenant-IdPour les appels de serveur à serveur qui ne portent aucun token utilisateur.
- 3Une correspondance sur un domaine personnaliséLe domaine propre du client, associé à son compte.
- 4Le sous-domaine de la plateformeSon nom sur notre domaine, tant qu'il n'apporte pas le sien.
- 5Un paramètre de query stringPar nom : uniquement en développementLa valeur de la liste la plus facile à saisir à la main — c'est pourquoi elle vient en dernier.
Un travail qui survit à un crash.
01
Le travail s'exécute
L'état change et est sauvegardé. Rien n'a encore touché le réseau.
02
L'annonce est stockée, pas envoyée
Le message dont le reste du système a besoin est écrit dans une table de base de données, à côté du travail qui l'a produit.
03
Un seul sweeper publie
Un processus d'arrière-plan balaie la table et publie ce qu'il y trouve. Rien d'autre dans la codebase ne parle au broker.
04
Les consumers s'attendent aux répétitions
La livraison a lieu au moins une fois. Les consumers vérifient une clé d'event traité ou s'appuient sur un index unique, si bien qu'une deuxième tentative échoue sans dommage.
Le nombre de retries est fini. Après un nombre fixe d'échecs, un message est marqué dead-letter et n'est plus réessayé — nous préférons le dire clairement plutôt que de laisser croire le contraire.
Les modules sont des invités dans le processus hôte.
Installer ou retirer un module ne demande ni redémarrage ni nouveau déploiement.
Chaque module est chargé dans son propre collectible assembly load context. En installer un ne redémarre pas l'hôte, et en retirer un non plus.
L'isolation est réelle, pas seulement de façade. Les dépendances propres d'un module se chargent en privé, à l'intérieur de celui-ci. Ce qui vient de l'hôte se limite à une liste courte et explicite de contrats partagés — le SDK de modules, EF Core, MediatR, FluentValidation et les bibliothèques de base d'ASP.NET et .NET — ainsi que les contract-assemblies que les modules publient les uns pour les autres.
La vérification qui nous importe est celle qui le prouve plutôt que de l'affirmer : charger un module, relâcher chaque référence vers lui, forcer une garbage collection, puis confirmer que la weak reference vers son load context est morte. Si un module ne pouvait pas réellement être déchargé, cette vérification échouerait.
La signature est une signature RSA sur un hash SHA-256, vérifiée contre une clé publique que l'opérateur installe. Installer depuis une URL exige une chose de plus qu'une signature valide : l'hôte source doit figurer sur une allow-list, et cette liste est livrée vide — installer depuis une URL ne fait donc rien tant qu'un opérateur n'y ajoute pas de source.
Publication — un événement au niveau de l'hôte
- La signature de l'artefact — une signature RSA sur un hash SHA-256 — est vérifiée contre une clé publique que l'opérateur installe.
- Les assemblies se chargent dans un assembly load context privé et collectible.
- Le manifeste enregistre le module dans la marketplace. Aucun redéploiement.
Installation — un événement au niveau du tenant
- Les migrations s'exécutent dans le schéma propre du module, à l'intérieur de la base de données de ce client.
- Le menu et la liste des autorisations se mettent à jour immédiatement.
- La désinstallation coupe le module et conserve ses données — supprimer les tables est un choix distinct et délibéré.
Un module déclare ce qu'il est dans un seul fichier :
{
"slug": "chat",
"name": "Chat",
"version": "1.0.0",
"sdkVersion": "1.1.0",
"entryAssembly": "CleverInit.Module.Chat.dll",
"schema": "chat",
"dependencies": [],
"frontend": {
"remoteEntry": "panel/remoteEntry.json",
"exposedModule": "./Routes"
},
"navSections": [
{ "items": [
{ "label": "Chat", "routerLink": null, "children": [
{ "label": "chat.menu.conversations",
"routerLink": "/m/chat/conversations" }
] }
] }
],
"permissions": [
{ "name": "Chat.View", "displayName": "View chat" },
{ "name": "Chat.ManageBots", "displayName": "Manage bots" }
],
"dashboardWidgets": [
{ "key": "chat.unread", "requiredPermission": "Chat.View" }
]
}La suite de tests est le contrat.
4 028 tests automatisés s'exécutent sur la host-solution — les suites de modules viennent s'y ajouter et sont suivies par module. La règle est binaire : terminé signifie que la solution entière ne signale aucun échec.
Tests de la host-solution par couche, tels que suivis dans le README du dépôt. Les suites de tests de modules sont comptées séparément.
Les objectifs de couverture par couche définis dans le charter, en pourcentage.
- Un build avec le moindre avertissement est un build incomplet. Le seuil est de 0 avertissement, 0 erreur — à chaque commit.
- Chaque module livre un test de parité : un controller qui demande une autorisation que son manifeste ne déclare pas fait échouer le build.
- Chaque changement significatif atterrit dans un journal d'audit append-only — append-only via l'application, pas scellé cryptographiquement contre quelqu'un ayant un accès direct à la base de données.
Épinglé, délibéré, ennuyeux.
Les versions sont épinglées et ajouter une bibliothèque exige une approbation explicite. Voici ce qui se trouve dans la solution aujourd'hui — pas une liste de souhaits.
Backend
.NET 10 · Clean Architecture · CQRS
- .NET 10 · C# 13 · ASP.NET Core
- EF Core 10 · SQL Server
- MediatR 14 · FluentValidation 12
- Mapperly 4.3 · Ardalis.Specification 9.3
- Redis · MassTransit + RabbitMQ 8.5
- BCrypt · Otp.NET · Asp.Versioning
- Scalar OpenAPI
- xUnit · Shouldly · NSubstitute
Panel
Angular 21 · zoneless · signals
- Angular 21, zoneless change detection
- État conservé dans des signals
- Frontends de modules comme remotes chargés à l'exécution
- Vitest
- anglais · néerlandais · allemand
Exploitation
Délibérément réduit
- Docker · docker-compose pour le démarrage local
- Spans OpenTelemetry dans le pipeline de requêtes
- Logging structuré — secrets, tokens et PII n'atteignent jamais un sink
- RFC 7807 sur toute la surface d'erreurs
Les chiffres derrière la connexion.
Un condensé pour le questionnaire — le tableau complet se trouve sur la page de sécurité.
- Access tokens
- Expirent au bout de 15 minutes. Les autorisations voyagent sous forme de claims, si bien que les contrôles d'autorisation sont des lookups en mémoire.
- Refresh tokens
- Valeurs aléatoires de 256 bits, stockées uniquement sous forme de hash, à usage unique, renouvelées à chaque refresh.
- Mots de passe
- BCrypt avec facteur de travail 12, fixé dans le code. Verrouillage après 5 tentatives échouées par défaut — configurable par tenant.
- Codes à usage unique
- Codes à 6 chiffres par e-mail ou SMS, hachés au repos, expirant au bout de 5 minutes avec un plafond de 3 tentatives.
Trois réponses franches.
- Il existe un portail API public. Il couvre les API publiques de la plateforme — exactement celles-là, et rien de plus. Ce qui est interne reste interne.
- Kubernetes est la direction que prend la plateforme. Le fournisseur qui l'exploitera, nous ne le publions pas volontairement — ce silence est une décision de sécurité, pas un oubli.
- Pas de facturation à l'usage ni de calcul au prorata — ni aujourd'hui ni prévu. Une facture que vous pouvez anticiper, voilà la fonctionnalité.
Vous voulez aller plus loin ?
Trente minutes avec les personnes qui l'ont écrit. Apportez votre question d'architecture la plus difficile.
Réserver un appel