Direct naar de inhoud

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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. 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.

  1. 01

    Logging

    Naam van het command en verstreken milliseconden. Payloads worden nooit gelogd.

  2. 02

    Tracing

    De volledige levenscyclus van de handler, verpakt in een OpenTelemetry-span.

  3. 03

    Validatie

    Elke geregistreerde validator draait; een fout wordt een 400 met fouten per veld.

  4. 04

    Cache-invalidatie

    Cachesleutels worden na een geslaagd command ongeldig gemarkeerd.

  5. 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.

Diagram van hoe de gegevens van een via aanmelding aangemaakt account gescheiden zijn: een eigen database, met daarin de tabellen van elke module in een eigen schema.

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.

  1. 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.
  2. 2De ondertekende tokenclaimVoor aanroepen naar onze API-host. Het token draagt onze handtekening; die wordt gecontroleerd voordat we de claim erin vertrouwen.
  3. 3De X-Tenant-Id-headerVoor server-naar-server-aanroepen naar onze API-host zonder gebruikerstoken.
  4. 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.

  1. 01

    Het werk gebeurt

    De toestand verandert en wordt opgeslagen. Niets heeft het netwerk nog aangeraakt.

  2. 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.

  3. 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.

  4. 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" }
  ]
}
Ingekort uit modules/chat/module.json.
Het declaratiebestand dat de host vertelt hoe een module heet, welke assembly geladen moet worden, welk databaseschema hij bezit en welke menu-items, rechten en dashboardwidgets hij toevoegt.

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.

Application2.998
Infrastructure1.932
Domain1.114
API-integratie542
Modulehost456
Module-SDK77

Tests van de host-solution per laag, zoals bijgehouden in de README van de repository. Modulesuites worden apart geteld, in de tabel hieronder.

Domain95%
Application85%
Infrastructure70%
API70%

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