diff --git a/docs/web/architecture.md b/docs/web/architecture.md
index de5614e4..07d465df 100644
--- a/docs/web/architecture.md
+++ b/docs/web/architecture.md
@@ -123,7 +123,8 @@ own
| Component | Description |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `Program.cs` | Registers MediatR (scanning every `Features.*` assembly), FluentValidation, and uses `AddApplicationPart` so each slice's controllers are discovered by MVC. |
+| `Program.cs` | Thin composition root: builds the host and calls the `ServiceCollectionExtensions` methods below. |
+| `ServiceCollectionExtensions.cs` | Registers MediatR (scanning every `Features.*` assembly), FluentValidation, and uses `AddApplicationPart` so each slice's controllers are discovered by MVC. |
| `GlobalException.cs` | Global exception filter (host-level cross-cutting concern). |
| `Authentication/JwtAuthenticationHandler.cs` | JWT auth scheme middleware. |
| Swagger/OpenAPI documentation, health check endpoints | Host-level documentation and health endpoints. |
diff --git a/docs/web/token-validation.md b/docs/web/token-validation.md
index bfb0ad65..aff82837 100644
--- a/docs/web/token-validation.md
+++ b/docs/web/token-validation.md
@@ -79,7 +79,8 @@ directly, bypassing the `Features.Users` slice entirely.
`TokenService` is registered by `Features.Users`' own `AddFeaturesUsers()`
extension method, except for the lower-level `ITokenInfrastructure` (JWT
signing/verification) it depends on, which is registered by the host
-(`API.Core/Program.cs`) since `JwtAuthenticationHandler` (host-level auth
+(`API.Core/ServiceCollectionExtensions.cs`, via `AddCoreInfrastructure()`,
+called from `Program.cs`) since `JwtAuthenticationHandler` (host-level auth
middleware) also depends on it directly.
### Integration points
diff --git a/web/backend/API/API.Core/API.Core.csproj b/web/backend/API/API.Core/API.Core.csproj
index 09478802..70efde10 100644
--- a/web/backend/API/API.Core/API.Core.csproj
+++ b/web/backend/API/API.Core/API.Core.csproj
@@ -7,6 +7,10 @@
Linux
+
+
+
+
diff --git a/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs b/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs
index 35915fe1..d838cd24 100644
--- a/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs
+++ b/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs
@@ -25,17 +25,13 @@ public class JwtAuthenticationHandler(
/// Validates the incoming request's bearer JWT access token and produces an authentication result.
///
///
- /// The signing secret is resolved first from the ACCESS_TOKEN_SECRET environment variable, falling
- /// back to the Jwt:SecretKey configuration value, to stay consistent with the secret source used
- /// when tokens are issued. Fails if the secret is not configured, the Authorization header is
+ /// The signing secret is resolved first from the ACCESS_TOKEN_SECRET environment variable.
+ /// Fails if the secret is not configured, the Authorization header is
/// missing or not Bearer-scheme, or the token fails validation.
///
protected override async Task HandleAuthenticateAsync()
{
- // Use the same access-token secret source as TokenService to avoid mismatched validation.
string? secret = configuration[ConfigurationKeys.AccessTokenSecret];
- if (string.IsNullOrWhiteSpace(secret))
- secret = configuration[ConfigurationKeys.JwtSecretKeyFallback];
if (string.IsNullOrWhiteSpace(secret))
return AuthenticateResult.Fail("JWT secret is not configured");
diff --git a/web/backend/API/API.Core/Controllers/NotFoundController.cs b/web/backend/API/API.Core/Controllers/NotFoundController.cs
deleted file mode 100644
index f232fc33..00000000
--- a/web/backend/API/API.Core/Controllers/NotFoundController.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-
-namespace API.Core.Controllers;
-
-///
-/// Handles requests that do not match any other route, used as the application's fallback controller.
-///
-///
-/// Wired up as the fallback target via app.MapFallbackToController("Handle404", "NotFound") in
-/// Program.cs.
-///
-[ApiController]
-[ApiExplorerSettings(IgnoreApi = true)]
-[Route("error")]
-public class NotFoundController : ControllerBase
-{
- ///
- /// Returns a 404 response for any request that did not match a route.
- ///
- [HttpGet("404")]
- public IActionResult Handle404()
- {
- return NotFound(new { message = "Route not found." });
- }
-}
diff --git a/web/backend/API/API.Core/Controllers/ProtectedController.cs b/web/backend/API/API.Core/Controllers/ProtectedController.cs
deleted file mode 100644
index 36964b99..00000000
--- a/web/backend/API/API.Core/Controllers/ProtectedController.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using Shared.Contracts;
-using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
-
-namespace API.Core.Controllers;
-
-///
-/// Sample controller demonstrating an endpoint that requires JWT authentication.
-///
-[ApiController]
-[Route("api/[controller]")]
-[Authorize(AuthenticationSchemes = "JWT")]
-public class ProtectedController : ControllerBase
-{
- ///
- /// Returns the authenticated caller's user ID and username extracted from the JWT claims.
- ///
- /// userId and username may be null if not present in the token.
- [HttpGet]
- public ActionResult> Get()
- {
- string? userId = User.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
- string? username = User.FindFirst(JwtRegisteredClaimNames.UniqueName)?.Value;
-
- return Ok(
- new ResponseBody