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
7,119 passed · 0 failed
- 7.119
- 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 uw 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.

Bij welke klant een request hoort, bepaalt het adres waaraan het is gestuurd. Machineaanroepen naar onze API-host, die zelf geen klant heeft, vallen terug op de drie stappen eronder.
- 1Het eigen webadres van de werkruimteHet eigen domein van de klant, of zijn naam op het onze. Een inlogtoken dat een andere werkruimte noemt, wordt daar geweigerd.
- 2De ondertekende tokenclaimVoor aanroepen naar onze API-host. Het token draagt onze handtekening; die wordt gecontroleerd voordat we de claim erin vertrouwen.
- 3De X-Tenant-Id-headerVoor server-naar-server-aanroepen naar onze API-host zonder gebruikerstoken.
- 4Een numerieke waarde in het adresAlleen webhook-endpointsAlleen gehonoreerd op de endpoints waar een betaaldienst naartoe post; daar authenticeert de eigen handtekening van de afzender het verzoek.
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.
7.119 geautomatiseerde tests draaien op de host-solution, en de twaalf modules die vandaag in de marketplace staan, leveren daarbovenop elk een eigen suite mee — samen nog eens 48.421. 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, in de tabel hieronder.
De dekkingsdoelen per laag uit het charter, in procenten.
Elke module heeft zijn eigen testsuite.
De balken hierboven tellen één repository. Elk van de twaalf modules hieronder is een eigen codebase met een eigen testsuite, en samen zijn ze een orde van grootte groter dan de host.
- Crowdfunding
- 31.416
- Verificatie
- 4.191
- CRM
- 4.063
- Helpdesk
- 2.629
- PSP-providers
- 1.225
- Abonnementen
- 942
- AI Copilot
- 837
- Betalingen
- 825
- Chat
- 727
- Facturatie
- 634
- Klanten
- 474
- Enquêteformulieren
- 458
Over deze twaalf modules
48.421
Gemeten door elke modulesuite volledig te draaien: elf op 9 september 2026 en de Helpdesk op 18 oktober 2026. Alle suites meldden nul fouten en niets overgeslagen. Modules die nog in ontwikkeling zijn, blijven buiten beschouwing: een testaantal bij een onafgemaakte module zou een uitspraak zijn over een opleverdatum.
- 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 · PostgreSQL
- 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. Ze dragen identiteit en rollen, nooit rechten — rechten worden één keer per request bepaald, eerst uit de cache, zodat autorisatiechecks lookups in het geheugen blijven.
- 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 u kunt voorspellen, dát is de feature.
Wilt u de diepte in?
Dertig minuten met de mensen die het schreven. Neem uw lastigste architectuurvraag mee.
Plan een gesprek