Adds the AI-DLC inception record for deploying the CMS as a single .NET application on hosting where no server configuration is possible. The reverse-engineering artifacts were regenerated: the previous set predated the Master module, the Slave host, the solution reorganisation and single-host serving, all of which matter for deployment. Findings were verified by running the build, both test suites and the linter rather than inferred, which surfaced two facts the plan depends on: the frontend lint gate currently fails (5 errors), and two transitive packages carry high-severity advisories. Records 24 functional requirements, 32 traced decisions and a seven-unit decomposition whose ordering is load-bearing: durability work must land before the first automated deploy, or the very first deploy is the one that silently breaks master/slave trust. Two conflicts found while designing and carried into the units: - Both modules call AddDataProtection(), which runs after the host and would override a persistent key store while still passing any registration test. - The availability gate runs before authentication, so its admin bypass cannot read HttpContext.User. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
17 KiB
17 KiB
Code Structure
Build System
- Type: .NET SDK (MSBuild /
dotnetCLI) for the backend; pnpm + Vite for the admin SPA. - Solution:
SlpModularCms.sln— 10 projects, organised into three top-level Solution Folders (seeCLAUDE.md/AGENTS.md):- Application —
SlpModularCms.Coreplus a nested Modules folder (Modules.Master,Modules.Identity,Modules.Availability) - Tests — mirrors Application, with its own nested Modules folder
- Clients — the deployable hosts:
SlpModularCms.Api,SlpModularCms.Api.Slave
- Application —
- Target framework:
net10.0for every project. SDK in use: 10.0.301. - Key build settings:
NullableandImplicitUsingsenabled everywhere.SlpModularCms.Coreuses<FrameworkReference Include="Microsoft.AspNetCore.App" />so a class library can depend on ASP.NET Core types. - Coverage:
coverlet.runsettingsat the repository root excludes migrations,obj/, generated OpenAPI interceptors, and anything marked[ExcludeFromCodeCoverage]. - Frontend build:
frontend/package.json—buildrunstsc -b && vite build;vite.config.tssetsbase: '/admin/'forcommand === 'build'only, so the dev server still serves from/. - Publish coupling:
SlpModularCms.Api.csprojdefines theBuildAndCopyAdminFrontendtarget withBeforeTargets="Publish", which runspnpm install --frozen-lockfileandpnpm buildinfrontend/and copiesfrontend/dist/**intowwwroot/admin/.dotnet publishtherefore requires Node and pnpm on the build machine.wwwroot/is gitignored.
Project Structure
graph TD
root["SlpModularCms (repo root)"]
sln["SlpModularCms.sln"]
src["src/"]
fe["frontend/ (admin SPA)"]
docs["aidlc-docs/"]
api["SlpModularCms.Api<br/>Client / host"]
slave["SlpModularCms.Api.Slave<br/>Client / host"]
core["SlpModularCms.Core<br/>shared library"]
mid["Modules.Identity"]
mav["Modules.Availability"]
mma["Modules.Master"]
tests["4 test projects<br/>Core, Identity, Availability, Master"]
root --> sln
root --> src
root --> fe
root --> docs
src --> api
src --> slave
src --> core
src --> mid
src --> mav
src --> mma
src --> tests
classDef client fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
classDef corelayer fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
classDef module fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef test fill:#e2e8f0,stroke:#4a5568,stroke-width:1px,color:#000;
classDef meta fill:#d6bcfa,stroke:#6b46c1,stroke-width:1px,color:#000;
class api,slave,fe client;
class core corelayer;
class mid,mav,mma module;
class tests test;
class root,sln,src,docs meta;
Text alternative: The repository root holds the solution file, a src/ folder with two host projects, Core, three modules and four test projects, plus a separate frontend/ admin SPA and the aidlc-docs/ documentation tree.
Key Classes/Modules
classDiagram
class IModule {
+string Name
+string Version
+RegisterServices(IServiceCollection)
+UseModule(IApplicationBuilder)
}
class ModuleOrchestrator {
+IReadOnlyList~string~ ModuleNames
+DiscoverModules()
+RegisterModuleServices(IServiceCollection)
+UseModules(IApplicationBuilder)
}
class IdentityModule
class AvailabilityModule
class MasterModule
class IAvailabilityService {
+IsAvailableAsync()
+UpdateStatusAsync()
}
class PersistentAvailabilityService
class IMasterAvailabilityService {
+GetMasterStatus()
}
class MasterAvailabilityService
IModule <|.. IdentityModule
IModule <|.. AvailabilityModule
IModule <|.. MasterModule
ModuleOrchestrator --> IModule
IAvailabilityService <|.. PersistentAvailabilityService
IMasterAvailabilityService <|.. MasterAvailabilityService
Text alternative: ModuleOrchestrator works against the IModule contract implemented by the three modules; the Availability module supplies the concrete availability and master-gate services behind Core's interfaces.
Existing Files Inventory
Clients (deployable hosts)
src/SlpModularCms.Api/Program.cs— Host composition: local settings overlay, module discovery, core infrastructure/CORS/rate limiting,/api/v1convention, enum-as-string JSON, exception handler, HTTPS redirect, static files, CORS, module middleware, auth, controllers, and the two SPA fallbacks (/admin/{*path:nonfile}→admin/index.html,{*path:nonfile}→index.html).src/SlpModularCms.Api/Program.Coverage.cs— Coverage-support partial.src/SlpModularCms.Api/SlpModularCms.Api.csproj— Package/project references plus theBuildAndCopyAdminFrontendpublish target.src/SlpModularCms.Api/appsettings.json— Production baseline with placeholder secrets;LoggingdefaultWarning;AllowedHosts: "*"; emptyCors:AllowedOrigins;JwtSettings,Availability,MasterModule,MasterPolling,RateLimitingsections.src/SlpModularCms.Api/appsettings.Development.json— LocalDB connection, dev JWT secret,CookieSameSite: None, CORS forlocalhost:5173, relaxed rate limits,MasterUrl: https://localhost:7221.src/SlpModularCms.Api/appsettings.local.json— Gitignored developer overrides.src/SlpModularCms.Api/Properties/launchSettings.json—http(5284) andhttps(7221) profiles, bothDevelopment, launching/scalar.src/SlpModularCms.Api.Slave/Program.cs,…/appsettings*.json,…/Properties/launchSettings.json— Second host on 7222; no Master module reference; expects its own database.
Core
src/SlpModularCms.Core/Hosting/ModuleOrchestrator.cs— GlobsSlpModularCms.Modules.*.dllfromAppDomain.CurrentDomain.BaseDirectory, loads assemblies, instantiates every non-abstractIModule, and exposesModuleNames. Failures are logged, not thrown.src/SlpModularCms.Core/Hosting/ServiceCollectionExtensions.cs—AddCoreInfrastructure(DbContext, Identity with password policy, JWT bearer withClockSkew.Zero, the three hierarchical policies, exception handler + ProblemDetails, API versioning, OpenAPI),AddCmsCors,AddCmsRateLimiting(fixed-windowlogin, sliding-windowrefresh).src/SlpModularCms.Core/Hosting/ApiPrefixConvention.cs— Applies the singleapi/v1prefix to every controller.src/SlpModularCms.Core/Hosting/SystemController.cs—GET /api/v1/System/capabilities, returns loaded module names.src/SlpModularCms.Core/Data/ApplicationDbContext.cs— Identity +RefreshToken,Invitation,ModulePermission,GlobalAvailabilityState.src/SlpModularCms.Core/Identity/Entities/*.cs—ApplicationUser,ApplicationRole,RefreshToken,Invitation,ModulePermission,GlobalAvailabilityState.src/SlpModularCms.Core/Identity/Models/*.cs—IdentityRequests,JwtSettings,TokenResponse.src/SlpModularCms.Core/Identity/Services/{AuthService,IAuthService,InvitationService,IInvitationService,SetupService}.cs— Login/refresh/revoke with token rotation, invitation lifecycle, first-Owner bootstrap.src/SlpModularCms.Core/Identity/Authorization/{HierarchicalRoleHandler,HierarchicalRoleRequirement}.cs— Owner > Administrator > User satisfaction.src/SlpModularCms.Core/Availability/{AvailabilityOptions,AvailabilityStatus,AvailabilityStatusDetails,IAvailabilityService,MasterControlledAvailabilityException}.cs— Availability contract; the exception is what turns a local override attempt into409 Conflictwhile master-controlled.src/SlpModularCms.Core/Exceptions/{GlobalExceptionHandler,ValidationException,UnauthorizedException,InvitationOrUserAlreadyExistsException}.cs— RFC 9457 mapping.src/SlpModularCms.Core/Modules/{IModule,ModuleInfo}.cs— Module contract.src/SlpModularCms.Core/Migrations/— 5 files;ApplicationDbContextmigrations, applied manually only.
Modules.Identity
Controllers/AuthController.cs—login(rate-limitedlogin),refresh(rate-limitedrefresh),revoke,change-password.Controllers/SetupController.cs—status,owner.Controllers/InvitationController.cs—validate,complete; anonymous.Controllers/UsersController.cs— list,meupdate,invite, role/active updates, delete;AdminOnlyby default.IdentityModule.cs— Module registration.
Modules.Availability
AvailabilityModule.cs— Registers services,AvailabilityDbContext, Data Protection, pollingHttpClientand hosted service; onUseModulerunsDatabase.Migrate()and installsAvailabilityMiddleware.Middleware/AvailabilityMiddleware.cs— Bypass prefixes, admin-token bypass, master gate then local status, 503ProblemDetailsotherwise.Services/PersistentAvailabilityService.cs— Persisted local status with caching/circuit breaker; throwsMasterControlledAvailabilityExceptionon local override while master-controlled.Services/{MasterAvailabilityService,MasterGateStatus,MasterStatusPollClient,MasterApiKeyProtector,…}.cs— Master gate state, poll client, encrypted key handling, DI dependency bundle.BackgroundServices/MasterStatusPollingBackgroundService.cs— Periodic pull with fail-open.Controllers/{AvailabilityController,MasterController}.cs— Public status + Owner-only update; inbound master register/status/registered-url.Data/AvailabilityDbContext.cs,Data/Entities/MasterRegistration.cs,Config/MasterPollingOptions.cs,Repositories/*,Models/MasterModels.cs;Migrations/— 5 files, auto-applied.
Modules.Master
MasterModule.cs— Data Protection,MasterModuleOptions,MasterDbContext, repositories/services,SlaveApiClientwith aslave-resiliencehandler (2 retries, exponential backoff with jitter, configurable timeout),IntegrityCheckBackgroundService,HttpContextAccessor; migrates onUseModule.Controllers/CmsInstanceController.cs— Owner-only list/create/update-status.Controllers/SlaveStatusController.cs— Anonymous pull endpoint for slaves.Services/{CmsInstanceService,SlaveApiClient,ApiKeyProtector,MasterServiceDependencies,…}.csBackgroundServices/IntegrityCheckBackgroundService.cs— Periodic reconciliation and status re-push.Data/MasterDbContext.cs,Data/Entities/{CmsInstance,CmsInstanceStatus}.cs,Models/*,Options/MasterModuleOptions.cs,Repositories/*;Migrations/— 3 files, auto-applied.
Tests
src/SlpModularCms.Core.Tests/— 7 files (Exceptions, Hosting, Identity).src/SlpModularCms.Modules.Identity.Tests/— 4 files (Controllers).src/SlpModularCms.Modules.Availability.Tests/— 10 files (Controllers, Services, Repositories, BackgroundServices).src/SlpModularCms.Modules.Master.Tests/— 7 files (Controllers, Services, Repositories, BackgroundServices).
frontend (admin SPA)
frontend/vite.config.ts—base: '/admin/'on build,@alias, dev port 5173, Vitest config with v8 coverage.frontend/package.json— Scriptsdev,dev:slave(modeslave, port 5174),dev:all(concurrently),build,lint,format,format:check,test,test:watch,test:coverage,preview.frontend/.env.example—VITE_API_BASE_URL,VITE_APP_TITLE, plus notes for the slave setup..env.local/.env.slave.localare local-only.frontend/src/lib/config.ts— ReadsVITE_API_BASE_URLandVITE_APP_TITLE; Zod-validatesapiBaseUrlas a URL, warning only in dev. Makes the production bundle environment-specific.frontend/src/lib/api-client.ts—fetchwrapper: credentials always sent, in-memory access token, 401 refresh-and-retry interceptor,ProblemDetailsErrorandNetworkError.frontend/src/router.tsx— TanStack Router withbasepath: import.meta.env.BASE_URL, so routing follows the/admin/base automatically.frontend/src/main.tsx— Sets document title from config, React Query client, optional MSW viaVITE_ENABLE_MSW, renders only after the initial silent refresh settles.frontend/src/api/use*.ts— Typed hooks per resource (availability, users, profile, invitation, setup, CMS instances, system capabilities), each with a colocated test.frontend/src/pages/— 10 pages, each with a test.frontend/src/components/{auth,cms,layout,shared,ui,users}/— Guards (ModuleGuard,RoleGuard), CMS instance dialogs/list, layout shell, shadcn-style primitives.frontend/src/{contexts,hooks,i18n,mocks,test}/— Auth provider, hooks, NL/EN translations, MSW handlers per domain, test setup.
Design Patterns
Module / plugin pattern
- Location:
Core/Modules/IModule.cs,Core/Hosting/ModuleOrchestrator.cs, each*Module.cs. - Purpose: Let one codebase deploy with different capability sets.
- Implementation: Reflection-based discovery of
SlpModularCms.Modules.*.dllin the app base directory; each module registers services and middleware itself. Deployment content, not configuration, decides capability.
Repository pattern
- Location:
Modules.Master/Repositories/,Modules.Availability/Repositories/. - Purpose: Keep EF Core access behind an interface so services stay unit-testable.
- Implementation: Interface plus EF-backed implementation per aggregate. Core's identity services use
ApplicationDbContext/Identity managers directly rather than repositories — an intentional inconsistency between old and new code.
Options pattern
- Location:
JwtSettings,AvailabilityOptions,MasterModuleOptions,MasterPollingOptions. - Purpose: Bind configuration sections to typed objects.
- Implementation:
services.Configure<T>/AddOptions<T>().BindConfiguration(...).
Dependency-bundle (parameter object)
- Location:
MasterServiceDependencies,MasterAvailabilityServiceDependencies. - Purpose: Keep constructors manageable where a service needs many collaborators.
Middleware gate
- Location:
AvailabilityMiddleware. - Purpose: Enforce availability centrally rather than per controller, with explicit bypasses.
Background reconciliation (push + pull)
- Location:
IntegrityCheckBackgroundService(master push),MasterStatusPollingBackgroundService(slave pull, fail-open). - Purpose: Make distributed status self-healing without a message broker.
JWT with refresh-token rotation
- Location:
AuthService,AuthController,frontend/src/lib/api-client.ts. - Purpose: Short-lived access tokens held in memory; rotating refresh token in an httpOnly cookie scoped to
/api/v1/auth.
Global exception handling to RFC 9457
- Location:
GlobalExceptionHandler+ typed exceptions. - Purpose: One error contract for all clients.
Resilience pipeline
- Location:
MasterModuleslave-resiliencehandler. - Purpose: Tolerate slow or briefly unreachable slaves without failing the Owner's action outright.
Critical Dependencies
Microsoft.EntityFrameworkCore.SqlServer — 10.0.9
- Usage: All three
DbContexttypes, one shared connection string. - Purpose: Persistence. Module contexts self-migrate; the Core context does not.
Microsoft.AspNetCore.Identity.EntityFrameworkCore — 10.0.9
- Usage:
ApplicationUser/ApplicationRolestores, password hashing, policy. - Purpose: Account primitives.
Microsoft.AspNetCore.Authentication.JwtBearer — 10.0.9
- Usage: Token validation in
AddCoreInfrastructurewithClockSkew.Zero. - Purpose: Stateless authentication. Requires
JwtSettings:Secretto be present or startup throws.
ASP.NET Core Data Protection (shared framework)
- Usage:
ApiKeyProtector,MasterApiKeyProtector. - Purpose: Encrypt slave API keys at rest. Default file-system key ring with no persistent store configured — a redeploy that loses the key folder makes stored keys unreadable.
Microsoft.Extensions.Http.Resilience — 9.6.0
- Usage:
SlaveApiClient. - Purpose: Retry and timeout for master→slave calls. Note: 9.x package on a
net10.0target.
Asp.Versioning.Mvc — 10.0.0
- Usage:
AddApiVersioningwithReportApiVersions. - Purpose: Version reporting alongside the static
/api/v1prefix convention.
Scalar.AspNetCore — 2.16.3
- Usage:
MapScalarApiReference(), Development only. - Purpose: API reference UI at
/scalar. Not exposed in production.
Vite 8 + React 19 + TanStack Router/Query (frontend)
- Usage: Admin SPA build and runtime.
- Purpose:
base: '/admin/'andbasepath: import.meta.env.BASE_URLare what make the/adminmount work.
pnpm (build-time, backend publish)
- Usage: Invoked from
SlpModularCms.Api.csprojduring publish. - Purpose: Builds the admin SPA. Makes Node + pnpm a hard prerequisite of
dotnet publish.