Built by engineers, for engineers

Architecture, security, and operations — laid out for the people who will inherit this platform.

Engineering at Clever Initiative

Every architectural decision is intentional. Nothing is included by accident.

This page is for the engineers who will live with this platform — the ones who own production, sign off on architecture reviews, and inherit the codebase. We will not soften anything for marketing here.

What follows is the truth about how Clever Initiative is engineered: the layering, the data isolation guarantees, the runtime module loading, the security model, the test discipline, and the operational shape. Every claim below is enforced in code, in CI, or in a hard boundary that cannot be silently crossed.

  • .NET 10 + Angular 21 — both intentionally on their latest stable major
  • Clean Architecture enforced at the project-reference level, not just by convention
  • Module marketplace loaded into private AssemblyLoadContexts with cryptographic signature verification

Ten engineering principles, every commit

These rules are non-negotiable. Each one is enforced in code review, in CI, or by the type system.

01

Clean Architecture

Domain ← Application ← Infrastructure ← API. Inner layers never reference outer. Adding a using of an outer layer in an inner project is a compile-time bug, not a shortcut.

02

CQRS via MediatR

Commands change state. Queries read state. They flow through separate pipelines (Logging → Validation → CacheInvalidation → Transaction). One handler per operation, validators co-located.

03

Result pattern

Result and Result<T> for every expected failure. Domain exceptions for invariant violations. No throw new Exception ever. Failure paths are explicit and typed.

04

AsNoTracking + Select projection

Zero exceptions on every read. No tracked entities returned to controllers. No N+1. Every list endpoint returns PagedResult<T> with pageSize capped at 100.

05

Defense in depth on authorization

Permissions are checked twice — at the controller via [HasPermission] and inside the handler via IPermissionService. Passing the controller alone is not sufficient.

06

Database-per-tenant

Architectural commitment, not a configuration flag. Each tenant gets its own SQL Server database. AES-256-encrypted connection strings stored in the host database. Zero shared rows.

07

Outbox-based integration events

Cross-process events are written into the same DB transaction as the state change. A worker publishes them via MassTransit + RabbitMQ. At-least-once delivery. Consumers idempotent by design.

08

No sensitive data in logs

Passwords, tokens, OTP codes, connection strings, mandate refs, card last-fours, raw webhook bodies — never logged at any level. The payment-gateways module ships its own ILogScrubber for this reason.

09

Domain constants over magic numbers

Every numeric or string limit in a validator references DomainConstants.*. New limit groups become a nested static class. No 100, 255, or 4096 ever inlined.

10

0 warnings policy

dotnet build and ng build both exit with 0 errors and 0 warnings. dotnet test CleverInit.slnx exits with 0 failed. Anything less is incomplete work, not a partial victory.

Backend architecture

Clean Architecture, .NET 10

The dependency arrows in the diagram below are enforced at the project-reference level. Cross them and the build fails — there is no convention-only layer policing here.

Domain
Application
Infrastructure
API (Presentation)
Composition root
01
DomainInnermost · 0 dependencies
Entities, value objects, domain events, domain exceptions, DomainConstants. Nothing else lives here.
02
ApplicationUse cases
Depends only on Domain. Commands, queries, MediatR handlers, FluentValidation validators, Mapperly mappers, MediatR pipeline behaviors.
03
InfrastructureAdapters
Depends on Application + Domain. EF Core 10, Redis, MassTransit, JWT, BCrypt, repositories. Implements contracts the Application layer defines.
04
API (Presentation)Composition root
Depends on all three. Controllers, middleware, DI composition, Scalar OpenAPI, RFC 7807 ProblemDetails error mapping.

MediatR pipeline (outer → inner)

Every command flows through this pipeline. Queries skip the transaction step.

01
LoggingBehavior
Logs command name and elapsed milliseconds. Never logs payload contents.
02
ValidationBehavior
Runs all registered FluentValidation validators. Throws ValidationException → 400 with field-level errors dictionary.
03
CacheInvalidationBehavior
Marks cache keys dirty after successful commands. Pattern-based invalidation via ICacheService (Redis in prod, in-memory in dev).
04
TransactionBehavior
Wraps each ICommand in an EF Core transaction. SaveChanges happens inside; integration events publish only after commit.
Multi-tenancy

Database-per-tenant. Complete data isolation. No shortcuts.

Database-per-tenant is an architectural commitment, not a configuration option. Each tenant gets its own SQL Server database. There are no shared tables and no row-level filters substituting for real isolation.

What this guarantees

  • Physical separation of customer data — never co-mingled, ever
  • Per-tenant migrations run independently, online, without affecting other tenants
  • Compliance contracts can be signed knowing data physically lives in one tenant boundary
  • A bug in row-level filtering cannot leak across tenants — there are no row-level filters
  • Backup, restore, and disaster recovery happen per tenant, with per-tenant RPO/RTO

How tenant connection strings are protected

  • Stored in the host database in the Tenant.ConnectionString column — encrypted with AES-256 via ASP.NET Core Data Protection
  • Decrypted in-process only when a tenant DbContext is requested
  • Never logged at any log level, never returned in any API response, never serialized to telemetry

Tenant resolution chain — 5 steps, first match wins

On every request, the platform identifies which tenant the request belongs to via this fallback chain. Resolution happens once per request, in middleware ordered after authentication so the JWT claim is available.

1
JWT tenant_id claim
If the access token carries tenant_id, that is authoritative.
2
X-Tenant-Id HTTP header
Platform admins acting on a specific tenant set this header explicitly.
3
Custom domain match
A tenant's own domain — app.theirclient.com — is mapped at startup and matched per request.
4
Platform subdomain
{slug}.cleverinit.com routes to the matching tenant.
5
?tenant= query string
Development-only. Disabled in production. The chain falls through to a 401 if no resolution succeeds.
Identity & Access

Layered authentication and authorization

Authentication and authorization are layered. Every request passes through both, and authorization is checked twice — at the HTTP boundary and again in the handler.

Login credential is emailOrPhone

Never username. Forms, models, error messages, and tests all match. Lockout after 5 failed attempts.

JWT access token, 15 minutes

HMAC-SHA256, carries the user's full permission set as a string-array claim. Authorization decisions are O(1) in-memory lookups — zero DB hits per request.

Refresh token, 7 days

Random 256-bit, persisted in the database, rotated on use. Compromise window is bounded; refresh tokens can be revoked at any time.

BCrypt cost factor 12

≈ 100 ms per hash on production hardware. Tuned to stay slow as Moore's law advances; cost is reviewed annually.

Two-factor authentication

OTP delivered via email or SMS, configurable per tenant. Otp.NET implementation; OTP codes never logged.

Defense-in-depth authorization

[HasPermission("Foo.Bar")] on the controller AND permissionService.HasAsync(...) re-checked in the handler. Permission strings must match the seeder exactly — wrong strings make actions silently invisible.

Event processing

The outbox pattern — at-least-once with effectively-once UX

Business operations that span system boundaries — sending a welcome email, provisioning a module database, notifying a third-party webhook — are dispatched through events. The platform never makes a synchronous call to an external service in the request path.

01
STEP 01
State change in DB transaction
Aggregate root mutates state inside ApplicationDbContext.SaveChangesAsync().
02
STEP 02
Outbox row, same transaction
Integration events for cross-process delivery are written to the OutboxMessages table inside the same DB transaction. Atomic with the state change.
03
STEP 03
OutboxProcessorWorker publishes
Background worker polls every 5 seconds, batches up to 50 events, publishes them via MassTransit to RabbitMQ. Marks rows as processed on success.
04
STEP 04
Idempotent consumers
Consumers are written so running the same event twice has the same effect as running it once. At-least-once delivery becomes effectively-once for the end user.
Module marketplace

Modules are guests in the host process

Modules ship as signed .tar.gz artifacts. The host loads each module's assemblies into a private AssemblyLoadContext. Tenants install modules independently into their own dedicated databases. No host redeploy is required to make a new module available, and no host redeploy is required when a tenant installs one.

Publish (host-scope, ops/CI-driven)

A module developer runs the marketplace publish pipeline; out comes a signed {slug}-{version}.tar.gz. The artifact is dropped into the host's Modules:RootPath. On first request that needs the slug, the host verifies the RSA-4096 signature, extracts the artifact, loads its assemblies into a private ALC, and registers the manifest. The module becomes available for tenants to install. No tenant starts using it yet.

Install (tenant-scope, admin-driven)

A tenant admin clicks Install on the marketplace page. The platform writes a TenantModule row, runs the module's migrations against the tenant's dedicated database, seeds the module's permissions for that tenant, fires an integration event, and the feature becomes live for that tenant's users on their next request. Installation does not load any new code — that already happened at publish time.

The hard boundaries — never cross these

Modules talk only via integration events
No DI calls between modules. No DbContext sharing. No raw SQL against another module's tables. If two modules need a tighter contract, expand one module's *.Contracts package — never weaken the boundary.
Modules reference Abstractions only
CleverInit.Abstractions, CleverInit.Module.Sdk, MediatR, FluentValidation, Mapperly, EF Core, Microsoft.Extensions.* — and any third-party NuGet they genuinely need. Never an internal core implementation assembly.
Cross-ALC type identity
When a third-party package is shared across module ALCs, its *.Abstractions co-package must be shared too. Otherwise cross-ALC types diverge and you get MissingMethodException at runtime.
Uninstall is non-destructive
OnUninstallAsync removes navigation entries and permissions only. Tenant data — CRM contacts, payment records, chat history — is preserved. Reinstall must be able to restore the state.
Frontend architecture

Angular 21, zoneless, signal-first

The admin panel is intentionally on the latest Angular stable. Every architectural decision pushes toward signal-based reactivity and OnPush change detection.

Zoneless change detection

provideZonelessChangeDetection() is active. zone.js is never imported. Change detection is driven entirely by signals.

OnPush, standalone, control flow

Every component is standalone with ChangeDetectionStrategy.OnPush. @if / @for / @switch only — never *ngIf / *ngFor / *ngSwitch. No NgModules anywhere.

NgRx SignalStore for shared state

Feature stores scoped via providers: [Store] on the host component — never providedIn: 'root'. Only LayoutStore and AuthStore are intentional global singletons.

ci-* shared component library

Every UI primitive — page header, filter card, table, modal, form input, save button, empty state, loading — is a ci-* component from @cleverinit/panel-shared. Hand-rolling a kt-input is a PR-blocking violation.

Module Federation for module remotes

Each module's frontend is a federation remote that exposes exactly one thing: a routes.ts. Routes are mounted under /m/<slug>/... by the host. Every @angular/* package is exact-pinned to the host's identical version.

Vitest + Playwright

Vitest for unit tests with flushEffects() in zoneless mode. Playwright for E2E. Karma, Jasmine, and Jest are explicitly forbidden.

Tech stack

Pinned, deliberate, boring

Versions are pinned. Adding a new library requires explicit approval. Below is what is in the solution today.

Backend

.NET 10 · Clean Architecture · CQRS
  • .NET 10 · C# 13 · ASP.NET Core
  • EF Core 10 · SQL Server 2022
  • MediatR 14 · FluentValidation 12
  • Riok.Mapperly 4.3 · Ardalis.Specification 9.3
  • StackExchange.Redis 2.12 · MassTransit + RabbitMQ 8.5
  • BCrypt.Net-Next 4.1 · Otp.NET 1.4 · SendGrid 9.29 · Twilio 7.7 · Scalar OpenAPI 2.6

Frontend

Angular 21 · Signals-first · Zoneless
  • Angular 21 · TypeScript 5.7
  • NgRx SignalStore · Angular Signals
  • Metronic Tailwind (pre-built) · KTui · Keenicons
  • @ngx-translate/core (en · nl · de)
  • Vitest + Playwright (Karma / Jasmine / Jest banned)
  • esbuild + Vite build pipeline

Platform & DevOps

Docker · Kubernetes · GitHub Actions
  • Docker · docker-compose for local development
  • GitHub Actions for CI/CD across api, panel, and modules
  • Azure Kubernetes Service (AKS) as production target
  • Health checks, diagnostics, OpenTelemetry-ready observability
  • Per-tenant connection strings, AES-256 at rest
  • HTTPS in transit, OWASP-aligned hardening, RFC 7807 errors
Quality & operations

Quality gates that ship every release

Every commit must clear these gates. CI enforces what review cannot, and review enforces what CI cannot.

0 warnings, 0 errors

dotnet build CleverInit.slnx, ng build --configuration=production, npm run lint — all exit clean. A warning is incomplete work.

Full-solution test suite

dotnet test CleverInit.slnx executes the entire backend test pyramid on every commit. Coverage targets: Domain >95%, Application >85%, Infrastructure >70%, API >70%.

Frontend test discipline

Vitest for components, services, and stores. Playwright for end-to-end journeys. flushEffects() after every state change in zoneless mode.

Module manifest/code parity test

Every module ships a parity test that fails the build if a controller demands a permission missing from module.json. Wrong strings cannot ship past CI.

Audit logging by interceptor

Every significant state change is captured automatically — before/after values, who, when, IP. Sensitive fields are excluded by allow-list. Audit rows are immutable once written.

RFC 7807 errors with plain-language detail

Domain exceptions map to typed problem responses: ValidationException → 400 + field-level errors, EntityNotFound → 404, Duplicate → 409, BusinessRuleViolation → 422. detail strings are complete sentences in plain language — never class names, never SQL fragments.

DevOps & signing

Operations, CI/CD, and the module signing pipeline

Operations are deliberate. The signing pipeline is the most operationally sensitive part of the platform — it gates every module that loads in production.

Docker, docker-compose, AKS

Local development runs on docker-compose; production runs on Azure Kubernetes Service. Health checks, readiness probes, and graceful shutdown are first-class.

GitHub Actions CI/CD

Per-repo workflows for api, panel, and modules. Modules use a reusable workflow that builds, signs, and publishes the artifact in a single deterministic step.

RSA-4096 module signing

Every module artifact is signed with a private key held only in repository secrets. The host verifies signatures with the public key bundled in CleverInit.ModuleHost.Lifecycle.ArtifactSignatureVerifier. Unsigned artifacts will not load in production.

Versioning enforced by CI

On a v* tag push, the workflow asserts module.json:version matches the tag exactly. A mismatch fails the build with a clear error. The version bump is part of the same PR as the schema migration that requires it.

Release-safe migrations

Every entity or configuration change ships an EF migration in the same commit. Migrations are append-only after release — never edited, only superseded by a new migration.

Observability-ready logging

Structured logging via Microsoft.Extensions.Logging. PII / secrets / OTP / webhook bodies excluded by ILogScrubber. Correlation IDs propagated through MediatR pipeline and MassTransit consumers.

Want to talk to our engineering team?

Architecture deep-dives, security questionnaires, performance characteristics, integration design — we are happy to go further than this page does.

Book an Architecture ReviewRead the Charter

Build smarter. Launch faster. Scale effortlessly.

Tell us where you want to take your business. We'll show you the fastest path to get there — whether you want a fully managed platform, the source code in your hands, or a fully branded product to sell to your own clients.