Gebouwd door engineers, voor engineers.
Architectuur, beveiliging en operations, uitgeschreven voor de mensen die met dit platform gaan integreren, er modules bovenop bouwen en de architectuurreview moeten aftekenen. Niets hier is afgezwakt voor marketing.
dotnet build CleverInit.slnx
0 Warning(s) · 0 Error(s)
dotnet test CleverInit.slnx
4,028 passed · 0 failed
- 4.028
- Geautomatiseerde tests, host-solution
- 0
- Toegestane buildwarnings
- 5
- Behaviors op elk command
- 5
- Stappen om een tenant te bepalen
Elke architectuurkeuze is bewust gemaakt. Niets zit er per ongeluk in.
Tien keuzes die al het andere hebben bepaald.
Elk van deze keuzes wordt afgedwongen in code of gecontroleerd in CI — niet in een wiki gezet in de hoop dat het goed komt.
- 01
Clean Architecture die de compiler afdwingt
Domain ← Application ← Infrastructure ← API zijn projectreferenties, geen afspraken. Een handler die de infrastructuur in grijpt, breekt de build.
- 02
Eén pipeline voor elk verzoek
Logging, tracing, validatie, cache-invalidatie, transactie — altijd in dezelfde volgorde, zodat timing, validatie en foutafhandeling zich op elk endpoint hetzelfde gedragen.
- 03
Een eigen database per geregistreerde klant
Een account dat via registratie ontstaat, krijgt zijn eigen SQL-database, en de connection string wordt versleuteld met ASP.NET Core Data Protection voordat die wordt opgeslagen.
- 04
Leesoperaties zijn projecties met een pagineringscontract
Elke leesoperatie is een no-tracking-projectie naar een DTO, en elk lijst-endpoint geeft een gepagineerd resultaat met een harde limiet terug. Een onbegrensde resultaatset is geen optie die de code biedt.
- 05
Geen raw SQL, nergens
Alleen geparametriseerde EF Core-query's. Gebruikersinvoer wordt nooit in SQL geïnterpoleerd, want het pad om die string te bouwen bestaat simpelweg niet.
- 06
Events die een crash overleven — met eerlijke semantiek
Wat aangekondigd moet worden, wordt samen met het werk zelf in de database opgeslagen en daarna door één sweeper gepubliceerd. Bezorging is at-least-once, het aantal retries is eindig, en dead-letters benoemen we — we verstoppen ze niet.
- 07
Migraties zijn append-only
Een uitgeleverde migratie wordt nooit aangepast. Elke schemawijziging is een nieuwe voorwaartse migratie, in dezelfde commit als de entiteitswijziging die erom vraagt.
- 08
API's zijn geversioneerd
De HTTP-laag draait op Asp.Versioning, dus een breaking change is een expliciete nieuwe versie — geen stille wijziging onder je integratie.
- 09
Fouten zijn RFC 7807, in gewone taal
Validatie wordt een 400 met fouten per veld, niet gevonden een 404, duplicaten een 409, bedrijfsregels een 422 — en de detail is altijd een volledige zin, nooit een klassenaam of een stuk SQL.
- 10
Modules moeten bewijzen dat ze unloaden
Laad een module, laat elke referentie los, forceer een garbage collection en controleer dat de zwakke referentie naar zijn load context dood is. Kan een module niet écht unloaden, dan faalt die check.
Vier lagen, één richting.
De afhankelijkheidspijlen hieronder zijn projectreferenties. Ze kruisen levert geen opmerking in de codereview op — het breekt de build.
- Domain Binnenste laag · nul afhankelijkheden
- Entiteiten, value objects, domain events, domain exceptions, constanten.
- Application Use cases
- Hangt alleen af van Domain. Commands, query's, handlers, validators, mappers, pipeline behaviors.
- Infrastructure Adapters
- Hangt af van Application en Domain. EF Core, Redis, MassTransit, JWT, BCrypt, repositories.
- API Composition root
- Hangt af van alle drie. Controllers, middleware, dependency injection, OpenAPI, problem-details-mapping.
Binnenste lagen verwijzen nooit naar buiten. De richting wordt afgedwongen waar niet over te discussiëren valt: in de projectbestanden.
Diagram: vier concentrische ringen — API als buitenste, daarna Infrastructure, dan Application, met Domain in de kern. Afhankelijkheidsreferenties wijzen alleen naar binnen; een referentie die naar buiten wijst, breekt de build.
De request-pipeline.
Elk command doorloopt dezelfde vijf behaviors, van buiten naar binnen. Query's slaan de transactiestap over.
01
Logging
Naam van het command en verstreken milliseconden. Payloads worden nooit gelogd.
02
Tracing
De volledige levenscyclus van de handler, verpakt in een OpenTelemetry-span.
03
Validatie
Elke geregistreerde validator draait; een fout wordt een 400 met fouten per veld.
04
Cache-invalidatie
Cachesleutels worden na een geslaagd command ongeldig gemarkeerd.
05
Transactie
Commands draaien in een EF Core-transactie. Query's slaan deze stap over.
Eén klant, één database.
Een account dat via aanmelding wordt aangemaakt, krijgt een eigen SQL-database, ingericht en gemigreerd op het moment dat het account ontstaat.
De connection string van die database wordt met ASP.NET Core Data Protection versleuteld voordat hij wordt opgeslagen.
Beeldvak — afbeelding volgt
Diagram: hoe de gegevens van één via aanmelding aangemaakt account gescheiden blijven — een eigen database, met daarin de schema's per module.
Uitzoeken bij welke klant een request hoort, is een keten van vijf stappen. Die stopt bij de eerste match.
- 1De ondertekende tokenclaimHet token draagt onze handtekening; die wordt gecontroleerd voordat we de claim erin vertrouwen.
- 2De X-Tenant-Id-headerVoor server-naar-server-aanroepen zonder gebruikerstoken.
- 3Een match op een eigen domeinHet eigen domein van de klant, gekoppeld aan het account.
- 4Het platformsubdomeinDe naam van de klant op ons domein, zolang die geen eigen domein meebrengt.
- 5Een querystringparameterOp naam: alleen in developmentDe waarde in dit rijtje die je het makkelijkst met de hand intikt — daarom staat hij achteraan.
Werk dat een crash overleeft.
01
Het werk gebeurt
De toestand verandert en wordt opgeslagen. Niets heeft het netwerk nog aangeraakt.
02
De aankondiging wordt opgeslagen, niet verstuurd
Het bericht dat de rest van het systeem nodig heeft, wordt in een databasetabel geschreven — naast het werk dat het opleverde.
03
Eén sweeper publiceert
Een achtergrondproces veegt de tabel leeg en publiceert wat het vindt. Niets anders in de codebase praat met de broker.
04
Consumers rekenen op herhaling
Bezorging is at-least-once. Consumers controleren een sleutel van verwerkte events of leunen op een unieke index, zodat een tweede poging zonder schade mislukt.
Het aantal retries is eindig. Na een vast aantal mislukkingen wordt een bericht als dead-letter gemarkeerd en niet opnieuw geprobeerd — dat zeggen we liever eerlijk dan dat we anders suggereren.
Modules zijn gasten in het hostproces.
Een module installeren of verwijderen vraagt geen herstart en geen nieuwe deploy.
Elke module wordt in een eigen collectible assembly load context geladen. Installeren herstart de host niet, en verwijderen ook niet.
De isolatie is echt, niet alleen op papier. De eigen dependencies van een module laden privé, binnen die module. Wat uit de host komt is een korte, expliciete lijst gedeelde contracten — de module-SDK, EF Core, MediatR, FluentValidation en de basisbibliotheken van ASP.NET en .NET — plus de contract-assembly's die modules voor elkaar publiceren.
De check waar het ons om gaat, bewijst dit in plaats van het alleen te stellen: laad een module, laat elke referentie ernaar los, forceer een garbage collection en controleer dan dat de weak reference naar zijn load context dood is. Als een module niet echt ontladen kan worden, faalt die check.
De handtekening is een RSA-handtekening over een SHA-256-hash, gecontroleerd tegen een publieke sleutel die de beheerder installeert. Installeren vanaf een URL vraagt bovenop een geldige handtekening nog één ding: de bronhost moet op een allow-list staan, en die lijst wordt leeg geleverd — installeren vanaf een URL doet dus niets totdat een beheerder er een bron aan toevoegt.
Publiceren — op hostniveau
- De handtekening van het artefact — een RSA-handtekening over een SHA-256-hash — wordt gecontroleerd tegen een publieke sleutel die de beheerder installeert.
- Assemblies laden in een eigen, collectible assembly load context.
- Het manifest registreert de module in de marketplace. Geen redeploy nodig.
Installeren — op tenantniveau
- Migraties draaien in het eigen schema van de module, binnen de database van die klant.
- Het menu en de rechtenlijst worden direct bijgewerkt.
- Verwijderen zet de module uit en bewaart de gegevens — tabellen droppen is een aparte, bewuste keuze.
Een module verklaart in één bestand wat hij is:
{
"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" }
]
}De testsuite is het contract.
4.028 geautomatiseerde tests draaien op de host-solution — modulesuites komen daar bovenop en worden per module bijgehouden. De regel is binair: klaar betekent dat de hele solution nul fouten meldt.
Tests van de host-solution per laag, zoals bijgehouden in de README van de repository. Modulesuites worden apart geteld.
De dekkingsdoelen per laag uit het charter, in procenten.
- Een build met ook maar één warning is een onafgemaakte build. De lat ligt op 0 warnings, 0 errors — bij elke commit.
- Elke module levert een pariteitstest mee: een controller die om een recht vraagt dat niet in zijn manifest staat, breekt de build.
- Elke betekenisvolle wijziging belandt in een append-only auditlogboek — append-only via de applicatie, niet cryptografisch verzegeld tegen iemand met directe toegang tot de database.
Vastgepind, bewust, saai.
Versies zijn vastgepind en een bibliotheek toevoegen vereist expliciete goedkeuring. Dit is wat er vandaag in de solution zit — geen verlanglijstje.
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 leeft in signals
- Module-frontends als remotes die tijdens runtime laden
- Vitest
- Engels · Nederlands · Duits
Operations
Bewust klein
- Docker · docker-compose voor lokaal opstarten
- OpenTelemetry-spans in de request-pipeline
- Gestructureerde logging — secrets, tokens en PII bereiken nooit een sink
- RFC 7807 voor elke fout die naar buiten gaat
De cijfers achter het inloggen.
Een samenvatting voor de vragenlijst — het volledige beeld staat op de beveiligingspagina.
- Access tokens
- Verlopen na 15 minuten. Rechten reizen mee als claims, dus autorisatiechecks zijn lookups in het geheugen.
- Refresh tokens
- Willekeurige 256-bits waarden, alleen als hash opgeslagen, eenmalig bruikbaar en bij elke refresh geroteerd.
- Wachtwoorden
- BCrypt met werkfactor 12, vastgelegd in code. Standaard vergrendeling na 5 mislukte pogingen — per tenant instelbaar.
- Eenmalige codes
- 6-cijferige codes via e-mail of sms, gehasht opgeslagen, verlopen na 5 minuten met een maximum van 3 pogingen.
Drie eerlijke antwoorden.
- Er is een publiek API-portaal. Het dekt de publieke API's van het platform — precies die, en niets meer. Wat intern is, blijft intern.
- Kubernetes is waar het platform naartoe gaat. Welke provider het gaat draaien, publiceren we bewust niet — die stilte is een beveiligingskeuze, geen vergissing.
- Geen facturering op basis van gebruik en geen verrekening naar rato — niet vandaag en niet gepland. Een rekening die je kunt voorspellen, dát is de feature.
Wil je de diepte in?
Dertig minuten met de mensen die het schreven. Neem je lastigste architectuurvraag mee.
Plan een gesprek