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

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.

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

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.

  1. 1De ondertekende tokenclaimHet token draagt onze handtekening; die wordt gecontroleerd voordat we de claim erin vertrouwen.
  2. 2De X-Tenant-Id-headerVoor server-naar-server-aanroepen zonder gebruikerstoken.
  3. 3Een match op een eigen domeinHet eigen domein van de klant, gekoppeld aan het account.
  4. 4Het platformsubdomeinDe naam van de klant op ons domein, zolang die geen eigen domein meebrengt.
  5. 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.

  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.

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.

Application2.213
Infrastructure754
Domain515
API-integratie312
Modulehost173
Module-SDK61

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

Domain95%
Application85%
Infrastructure70%
API70%

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