Von Entwicklern gebaut, für Entwickler.
Architektur, Sicherheit und Betrieb — dargelegt für diejenigen, die diese Plattform integrieren, Module darauf aufbauen und das Architektur-Review abzeichnen werden. Nichts davon ist fürs Marketing geschönt.
dotnet build CleverInit.slnx
0 Warning(s) · 0 Error(s)
dotnet test CleverInit.slnx
4,028 passed · 0 failed
- 4.028
- Automatisierte Tests, Host-Solution
- 0
- Tolerierte Build-Warnungen
- 5
- Behaviors pro Command
- 5
- Schritte, um einen Tenant zu bestimmen
Jede Architekturentscheidung ist bewusst getroffen. Nichts ist zufällig enthalten.
Zehn Entscheidungen, die alles Weitere geprägt haben.
Jede davon wird im Code erzwungen oder in der CI geprüft — nicht in ein Wiki geschrieben in der Hoffnung, dass es schon klappt.
- 01
Clean Architecture, die der Compiler erzwingt
Domain ← Application ← Infrastructure ← API sind Projektreferenzen, keine Konventionen. Ein Handler, der in die Infrastruktur greift, lässt den Build fehlschlagen.
- 02
Eine Pipeline für jede Anfrage
Logging, Tracing, Validierung, Cache-Invalidierung, Transaktion — jedes Mal in derselben Reihenfolge, damit Timing, Validierung und Fehlersemantik über alle Endpunkte hinweg einheitlich bleiben.
- 03
Eine Datenbank pro registriertem Kunden
Ein über die Registrierung angelegtes Konto erhält seine eigene SQL-Datenbank, und dessen Connection String wird über ASP.NET Core Data Protection verschlüsselt, bevor er gespeichert wird.
- 04
Leseoperationen sind Projektionen mit Paginierungsvertrag
Jede Leseoperation ist eine No-Tracking-Projektion auf ein DTO, und jeder Listen-Endpunkt gibt ein paginiertes Ergebnis mit hartem Limit zurück. Eine unbegrenzte Ergebnismenge bietet der Code schlicht nicht an.
- 05
Kein rohes SQL, nirgendwo
Nur parametrisierte EF-Core-Abfragen. Benutzereingaben werden nie in SQL interpoliert, weil der Pfad zum Zusammenbauen solcher Strings schlicht nicht existiert.
- 06
Events, die einen Absturz überstehen — mit ehrlicher Semantik
Was angekündigt werden muss, wird gemeinsam mit der Arbeit selbst in der Datenbank gespeichert und anschließend von einem einzigen Sweeper veröffentlicht. Die Zustellung erfolgt mindestens einmal, die Zahl der Retries ist endlich, und Dead-Letters werden benannt, nicht verschwiegen.
- 07
Migrationen sind append-only
Eine ausgelieferte Migration wird nie geändert. Jede Schemaänderung ist eine neue Vorwärtsmigration, im selben Commit wie die Entitätsänderung, die sie erfordert.
- 08
APIs sind versioniert
Die HTTP-Oberfläche läuft auf Asp.Versioning, sodass ein Breaking Change eine explizite neue Version ist statt einer stillen Änderung unter Ihrer Integration.
- 09
Fehler sind RFC 7807, in verständlicher Sprache
Validierung wird zu 400 mit Fehlern je Feld, „Nicht gefunden“ zu 404, Duplikate zu 409, Geschäftsregeln zu 422 — und das Detail ist stets ein vollständiger Satz, nie ein Klassenname oder ein SQL-Fragment.
- 10
Module müssen beweisen, dass sie sich entladen
Laden Sie ein Modul, geben Sie jede Referenz frei, erzwingen Sie eine Garbage Collection und prüfen Sie, dass die schwache Referenz auf seinen Load Context tot ist. Kann ein Modul sich nicht wirklich entladen, schlägt diese Prüfung fehl.
Vier Schichten, eine Richtung.
Die Abhängigkeitspfeile unten sind Projektreferenzen. Sie zu überschreiten erzeugt keinen Kommentar im Code-Review — es lässt den Build fehlschlagen.
- Domain Innerste Schicht · null Abhängigkeiten
- Entitäten, Value Objects, Domain Events, Domain Exceptions, Konstanten.
- Application Use Cases
- Hängt nur von Domain ab. Commands, Queries, Handler, Validatoren, Mapper, Pipeline-Behaviors.
- Infrastructure Adapter
- Hängt von Application und Domain ab. EF Core, Redis, MassTransit, JWT, BCrypt, Repositories.
- API Composition Root
- Hängt von allen dreien ab. Controller, Middleware, Dependency Injection, OpenAPI, Problem-Details-Mapping.
Innere Schichten verweisen nie nach außen. Die Richtung wird dort erzwungen, wo sich nicht darüber streiten lässt: in den Projektdateien.
Diagramm: vier konzentrische Ringe — API ganz außen, dann Infrastructure, dann Application, mit Domain im Kern. Abhängigkeitsreferenzen zeigen nur nach innen; eine Referenz, die nach außen zeigt, lässt den Build fehlschlagen.
Die Request-Pipeline.
Jedes Command durchläuft dieselben fünf Behaviors, von außen nach innen. Queries reichen den Transaktionsschritt unangetastet durch.
01
Logging
Name des Commands und die verstrichenen Millisekunden. Payloads werden nie geloggt.
02
Tracing
Der vollständige Lebenszyklus des Handlers, eingebettet in einen OpenTelemetry-Span.
03
Validierung
Jeder registrierte Validator läuft; ein Fehler wird zu einer 400 mit Fehlern je Feld.
04
Cache-Invalidierung
Cache-Schlüssel werden nach einem erfolgreichen Command als ungültig markiert.
05
Transaktion
Commands laufen in einer EF-Core-Transaktion. Queries überspringen diesen Schritt.
Ein Kunde, eine Datenbank.
Ein über die Anmeldung angelegtes Konto erhält eine eigene SQL-Datenbank, die in dem Moment bereitgestellt und migriert wird, in dem das Konto entsteht.
Der Connection String dieser Datenbank wird mit ASP.NET Core Data Protection verschlüsselt, bevor er gespeichert wird.
Bildplatz — Abbildung folgt
Diagramm: wie die Daten eines einzelnen über die Anmeldung angelegten Kontos getrennt bleiben — eine eigene Datenbank, mit den Modul-Schemas darin.
Herauszufinden, zu welchem Kunden eine Anfrage gehört, ist eine Kette aus fünf Schritten. Sie hält beim ersten Treffer an.
- 1Der signierte Token-ClaimDas Token trägt unsere Signatur; sie wird geprüft, bevor wir dem Claim darin vertrauen.
- 2Der X-Tenant-Id-HeaderFür Server-zu-Server-Aufrufe, die kein Benutzer-Token tragen.
- 3Ein Treffer auf einer eigenen DomainDie eigene Domain des Kunden, seinem Konto zugeordnet.
- 4Die Plattform-SubdomainDer Name des Kunden auf unserer Domain, solange er keine eigene mitbringt.
- 5Ein Query-String-ParameterPer Name: nur in der EntwicklungDer Wert in dieser Liste, der sich am leichtesten von Hand eintippen lässt — deshalb steht er an letzter Stelle.
Arbeit, die einen Absturz übersteht.
01
Die Arbeit geschieht
Der Zustand ändert sich und wird gespeichert. Noch hat nichts das Netzwerk berührt.
02
Die Ankündigung wird gespeichert, nicht versendet
Die Nachricht, die der Rest des Systems braucht, wird in eine Datenbanktabelle geschrieben — neben der Arbeit, die sie erzeugt hat.
03
Ein einziger Sweeper veröffentlicht
Ein Hintergrundprozess durchläuft die Tabelle und veröffentlicht, was er findet. Nichts anderes in der Codebase spricht mit dem Broker.
04
Consumer rechnen mit Wiederholungen
Die Zustellung erfolgt mindestens einmal. Consumer prüfen einen Schlüssel für verarbeitete Events oder stützen sich auf einen eindeutigen Index, sodass ein zweiter Versuch folgenlos fehlschlägt.
Die Zahl der Retries ist endlich. Nach einer festen Zahl von Fehlversuchen wird eine Nachricht als Dead-Letter markiert und nicht weiter versucht — das sagen wir lieber offen, als das Gegenteil zu suggerieren.
Module sind Gäste im Host-Prozess.
Ein Modul zu installieren oder zu entfernen erfordert weder einen Neustart noch ein erneutes Deployment.
Jedes Modul wird in einen eigenen collectible Assembly Load Context geladen. Ein Modul zu installieren startet den Host nicht neu — und es zu entfernen genauso wenig.
Die Isolation ist echt und nicht nur nominell. Die eigenen Dependencies eines Moduls laden privat, innerhalb dieses Moduls. Vom Host kommt eine kurze, explizite Liste gemeinsam genutzter Verträge — das Modul-SDK, EF Core, MediatR, FluentValidation und die Basisbibliotheken von ASP.NET und .NET — sowie die Contract-Assemblies, die Module füreinander veröffentlichen.
Die Prüfung, auf die es uns ankommt, beweist das, statt es nur zu behaupten: ein Modul laden, jede Referenz darauf loslassen, eine Garbage Collection erzwingen und anschließend prüfen, dass die Weak Reference auf seinen Load Context tot ist. Ließe sich ein Modul nicht wirklich entladen, schlägt diese Prüfung fehl.
Die Signatur ist eine RSA-Signatur über einen SHA-256-Hash, geprüft gegen einen öffentlichen Schlüssel, den der Betreiber installiert. Die Installation von einer URL verlangt zusätzlich zu einer gültigen Signatur noch etwas: Der Quell-Host muss auf einer Allow-List stehen, und diese Liste wird leer ausgeliefert — die Installation von einer URL bewirkt also nichts, bis ein Betreiber eine Quelle hinzufügt.
Veröffentlichen — ein Host-Ereignis
- Die Signatur des Artefakts — eine RSA-Signatur über einem SHA-256-Hash — wird gegen einen öffentlichen Schlüssel geprüft, den der Betreiber installiert.
- Assemblies laden in einen privaten, collectible Assembly Load Context.
- Das Manifest registriert das Modul im Marketplace. Kein Redeploy.
Installieren — ein Tenant-Ereignis
- Migrationen laufen im eigenen Schema des Moduls, innerhalb der Datenbank dieses Kunden.
- Das Menü und die Berechtigungsliste werden sofort aktualisiert.
- Die Deinstallation schaltet das Modul ab und behält seine Daten — das Löschen von Tabellen ist eine separate, bewusste Entscheidung.
Ein Modul erklärt in einer einzigen Datei, was es ist:
{
"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" }
]
}Die Testsuite ist der Vertrag.
4.028 automatisierte Tests laufen auf der Host-Solution — Modul-Suites kommen obendrauf und werden pro Modul erfasst. Die Regel ist binär: Fertig heißt, dass die gesamte Solution null Fehler meldet.
Tests der Host-Solution nach Schicht, wie im README des Repositorys erfasst. Modul-Testsuites werden separat gezählt.
Die Coverage-Ziele je Schicht aus dem Charter, in Prozent.
- Ein Build mit auch nur einer Warnung ist ein unvollständiger Build. Die Grenze liegt bei 0 Warnungen, 0 Fehlern — bei jedem Commit.
- Jedes Modul liefert einen Paritätstest mit: Ein Controller, der eine Berechtigung anfordert, die sein Manifest nicht deklariert, lässt den Build fehlschlagen.
- Jede bedeutsame Änderung landet in einem append-only geführten Audit-Log — append-only über die Anwendung, nicht kryptografisch versiegelt gegen jemanden mit direktem Datenbankzugriff.
Fest gepinnt, bewusst, langweilig.
Versionen sind fest gepinnt, und eine Bibliothek hinzuzufügen erfordert ausdrückliche Genehmigung. Das ist, was heute in der Solution steckt — kein Wunschzettel.
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
- State liegt in Signals
- Modul-Frontends als zur Laufzeit geladene Remotes
- Vitest
- Englisch · Niederländisch · Deutsch
Betrieb
Bewusst klein
- Docker · docker-compose zum lokalen Hochfahren
- OpenTelemetry-Spans in der Request-Pipeline
- Strukturiertes Logging — Secrets, Tokens und PII erreichen nie einen Sink
- RFC 7807 über die gesamte Fehleroberfläche
Die Zahlen hinter der Anmeldung.
Ein Kurzüberblick für den Fragebogen — das vollständige Bild finden Sie auf der Sicherheitsseite.
- Access tokens
- Laufen nach 15 Minuten ab. Berechtigungen reisen als Claims mit, sodass Autorisierungsprüfungen In-Memory-Lookups sind.
- Refresh tokens
- Zufällige 256-Bit-Werte, nur als Hash gespeichert, einmalig verwendbar und bei jeder Erneuerung rotiert.
- Passwörter
- BCrypt mit Arbeitsfaktor 12, fest im Code verankert. Standardmäßig Sperrung nach 5 fehlgeschlagenen Versuchen — pro Tenant konfigurierbar.
- Einmalcodes
- 6-stellige Codes per E-Mail oder SMS, im Ruhezustand gehasht, verfallen nach 5 Minuten mit einer Obergrenze von 3 Versuchen.
Drei klare Antworten.
- Es gibt ein öffentliches API-Portal. Es deckt die öffentlichen APIs der Plattform ab — genau diese und nicht mehr. Was intern ist, bleibt intern.
- Kubernetes ist das Ziel, auf das die Plattform zusteuert. Welcher Anbieter es betreiben wird, veröffentlichen wir bewusst nicht — dieses Schweigen ist eine Sicherheitsentscheidung, kein Versäumnis.
- Keine nutzungsbasierte Abrechnung und keine anteilige Verrechnung — weder heute noch geplant. Eine Rechnung, die Sie vorhersehen können, ist das Feature.
Möchten Sie tiefer einsteigen?
Dreißig Minuten mit den Menschen, die es geschrieben haben. Bringen Sie Ihre schwierigste Architekturfrage mit.
Gespräch buchen