Built by engineers, for engineers.
Architecture, security and operations, laid out for the people who will integrate with this platform, build modules on top of it, and sign off on the architecture review. Nothing here is softened for marketing.
dotnet build CleverInit.slnx
0 Warning(s) · 0 Error(s)
dotnet test CleverInit.slnx
4,028 passed · 0 failed
- 4,028
- Automated tests, host solution
- 0
- Build warnings tolerated
- 5
- Behaviors on every command
- 5
- Steps to resolve a tenant
Every architectural decision is intentional. Nothing is included by accident.
Ten decisions that shaped everything else.
Each of these is enforced in code or checked in CI — not written in a wiki and hoped for.
- 01
Clean Architecture the compiler enforces
Domain ← Application ← Infrastructure ← API are project references, not conventions. A handler that reaches into infrastructure fails the build.
- 02
One pipeline for every request
Logging, tracing, validation, cache invalidation, transaction — the same order every time, so timing, validation and error semantics stay uniform across every endpoint.
- 03
A database per signup customer
An account created through signup gets its own SQL database, and its connection string is encrypted through ASP.NET Core Data Protection before it is stored.
- 04
Reads are projections with a pagination contract
Every read is a no-tracking projection to a DTO, and every list endpoint returns a paged result with a hard cap. An unbounded result set is not an option the code offers.
- 05
No raw SQL, anywhere
Only parameterized EF Core queries. User input is never interpolated into SQL, because the string-building path simply does not exist.
- 06
Events that survive a crash — with honest semantics
What must be announced is stored in the database alongside the work itself, then published by one sweeper. Delivery is at-least-once, retries are finite, and dead-letters are stated, not hidden.
- 07
Migrations are append-only
A shipped migration is never edited. Every schema change is a new forward migration, in the same commit as the entity change that needs it.
- 08
APIs are versioned
The HTTP surface runs on Asp.Versioning, so a breaking change is an explicit new version rather than a silent edit under your integration.
- 09
Errors are RFC 7807, in plain language
Validation maps to 400 with a field-level error map, not-found to 404, duplicates to 409, business rules to 422 — and the detail is always a complete sentence, never a class name or a SQL fragment.
- 10
Modules must prove they unload
Load a module, drop every reference, force a garbage collection, assert the weak reference to its load context is dead. If a module cannot genuinely unload, that check fails.
Four layers, one direction.
The dependency arrows below are project references. Crossing them does not produce a code review comment — it fails the build.
- Domain Innermost · zero dependencies
- Entities, value objects, domain events, domain exceptions, constants.
- Application Use cases
- Depends only on Domain. Commands, queries, handlers, validators, mappers, pipeline behaviors.
- Infrastructure Adapters
- Depends on Application and Domain. EF Core, Redis, MassTransit, JWT, BCrypt, repositories.
- API Composition root
- Depends on all three. Controllers, middleware, dependency injection, OpenAPI, problem-details mapping.
Inner layers never reference outward. The direction is enforced where it cannot be argued with: in the project files.
Diagram: four concentric rings — API outermost, then Infrastructure, then Application, with Domain at the core. Dependency references point inward only; a reference pointing outward fails the build.
The request pipeline.
Every command flows through the same five behaviors, outermost first. Queries pass the transaction step through untouched.
01
Logging
Command name and elapsed milliseconds. Payloads are never logged.
02
Tracing
The full handler lifecycle wrapped in an OpenTelemetry span.
03
Validation
Every registered validator runs; failure becomes a 400 with a field-level error map.
04
Cache invalidation
Cache keys marked dirty after a successful command.
05
Transaction
Commands wrapped in an EF Core transaction. Queries skip this step.
One customer, one database.
An account created through signup gets its own SQL database, provisioned and migrated the moment the account is created.
The connection string for that database is encrypted through ASP.NET Core Data Protection before it is stored.
Image slot — asset to come
Diagram: how one signup account's data is kept separate — its own database, module schemas inside it.
Working out which customer a request belongs to is a five-step chain. It stops at the first match.
- 1The signed token claimThe token carries our signature, which is checked before the claim inside it is trusted.
- 2The X-Tenant-Id headerFor server-to-server calls that carry no user token.
- 3A custom domain matchThe customer's own domain, mapped to their account.
- 4The platform subdomainTheir name on our domain, when they have not brought their own.
- 5A query-string parameterBy name: development onlyThe easiest value in the list to type by hand, which is why it sits last.
Work that survives a crash.
01
The work happens
State changes and is saved. Nothing has touched the network yet.
02
The announcement is stored, not sent
The message the rest of the system needs is written into a database table, alongside the work that produced it.
03
One sweeper publishes
A background processor sweeps the table and publishes what it finds. Nothing else in the codebase talks to the broker.
04
Consumers expect repeats
Delivery is at-least-once. Consumers check a processed-event key or lean on a unique index, so a second attempt fails harmlessly.
Retries are finite. After a fixed number of failures a message is marked dead-lettered and stops being retried — we would rather say that than imply otherwise.
Modules are guests in the host process.
Installing or removing one needs no restart and no redeploy.
Each module is loaded into its own collectible assembly load context. Installing one does not restart the host, and neither does removing one.
The isolation is real rather than nominal. A module's own dependencies load privately to it. What comes from the host is a short, explicit list of shared contracts — the module SDK, EF Core, MediatR, FluentValidation, and the ASP.NET and .NET base libraries — plus the contract assemblies modules publish for one another.
The check we care about is the one that proves this rather than asserting it: load a module, drop every reference to it, force a garbage collection, then confirm the weak reference to its load context is dead. If a module could not genuinely be unloaded, that check fails.
The signature is an RSA signature over a SHA-256 hash, checked against a public key the operator installs. Installing from a URL needs one more thing on top of a valid signature: the source host has to be on an allow-list, and that list ships empty, so install-from-URL does nothing until an operator adds a source to it.
Publish — a host event
- The artifact's signature — an RSA signature over a SHA-256 hash — is verified against a public key the operator installs.
- Assemblies load into a private, collectible assembly load context.
- The manifest registers the module in the marketplace. No redeploy.
Install — a tenant event
- Migrations run in the module's own schema, inside that customer's database.
- The menu and the permission list update immediately.
- Uninstall switches the module off and keeps its data — dropping tables is a separate, deliberate choice.
A module declares what it is in a single file:
{
"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" }
]
}The test suite is the contract.
4,028 automated tests run on the host solution — module suites run on top of that, tracked per module. The rule is binary: done means the whole solution reports zero failures.
Host-solution tests by layer, as tracked in the repository README. Module test suites are counted separately.
The charter's per-layer coverage targets, in percent.
- A build with any warning is an incomplete build. The gate is 0 warnings, 0 errors — every commit.
- Every module ships a parity test: a controller asking for a permission its manifest does not declare fails the build.
- Every meaningful change lands in an append-only audit history — append-only through the application, not cryptographically sealed against someone with direct database access.
Pinned, deliberate, boring.
Versions are pinned and adding a library requires explicit approval. This is what is in the solution today — not a wishlist.
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 held in signals
- Module front-ends as runtime-loaded remotes
- Vitest
- English · Dutch · German
Operations
Deliberately small
- Docker · docker-compose for local spin-up
- OpenTelemetry spans in the request pipeline
- Structured logging — secrets, tokens and PII never reach a sink
- RFC 7807 across the whole error surface
The numbers behind sign-in.
A digest for the questionnaire — the full picture lives on the security page.
- Access tokens
- Expire in 15 minutes. Permissions travel as claims, so authorization checks are in-memory lookups.
- Refresh tokens
- 256-bit random values, stored only as a hash, single-use, rotated on every refresh.
- Passwords
- BCrypt at work factor 12, fixed in code. Lockout after 5 failed attempts by default — tenant-configurable.
- One-time codes
- 6-digit codes by email or SMS, hashed at rest, expiring in 5 minutes with a 3-attempt cap.
Three straight answers.
- There is a public API portal. It covers the platform's public APIs — exactly those, and nothing more. What is internal stays internal.
- Kubernetes is where the platform is headed. Which provider will run it is something we deliberately do not publish — that silence is a security decision, not an oversight.
- No usage-based billing and no proration — not today, and not planned. A bill you can predict is the feature.
Want to go deeper?
Thirty minutes with the people who wrote it. Bring your hardest architecture question.
Book a call