# Business Logic Model — U1 Hosting & Serving
**Unit**: U1 Hosting & Serving
**Requirements**: FR-07, FR-10, FR-24
---
## 1. Scope of the Logic
U1 contains no domain business logic in the usual sense — no customer, order or invoice. What it does contain is **request-resolution logic**: given an incoming path, which of three co-hosted surfaces should answer, and under what conditions may the answer be suppressed.
Three decisions are made per request:
1. Which static mount, if any, owns this path
2. Whether the availability gate applies
3. Which fallback resolves a client-side route
---
## 2. Request Resolution Flow
```mermaid
graph TD
req["Incoming request"]
hdr["Security headers register
response-start callback"]
adminmount{"Path starts with /admin ?"}
adminslash{"Path is exactly /admin
without trailing slash ?"}
redirect["308 redirect to /admin/"]
adminfile{"File exists in
wwwroot/admin ?"}
serveadmin["Serve admin asset"]
webfile{"File exists in
wwwroot/web ?"}
serveweb["Serve website asset"]
gate["Availability gate"]
bypass{"Bypass prefix
or valid admin token ?"}
blocked["503 ProblemDetails"]
route["Routing"]
health{"Path is /health ?"}
healthresp["Health report"]
api{"Path starts with /api/v1 ?"}
ctrl["Controller"]
fallback{"Path has a file extension ?"}
notfound["404"]
whichspa{"Path starts with /admin ?"}
adminindex["Serve wwwroot/admin/index.html"]
webindex{"wwwroot/web/index.html
exists ?"}
serveindex["Serve website index.html"]
placeholder["Serve built-in placeholder page"]
req --> hdr
hdr --> adminmount
adminmount -->|yes| adminslash
adminslash -->|yes| redirect
adminslash -->|no| adminfile
adminfile -->|yes| serveadmin
adminfile -->|no| gate
adminmount -->|no| webfile
webfile -->|yes| serveweb
webfile -->|no| gate
gate --> bypass
bypass -->|no, and disabled| blocked
bypass -->|yes or available| route
route --> health
health -->|yes| healthresp
health -->|no| api
api -->|yes| ctrl
api -->|no| fallback
fallback -->|yes| notfound
fallback -->|no| whichspa
whichspa -->|yes| adminindex
whichspa -->|no| webindex
webindex -->|yes| serveindex
webindex -->|no| placeholder
classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
classDef serve fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef bad fill:#fbb6ce,stroke:#b83280,stroke-width:1px,color:#000;
class req,hdr entry;
class adminmount,adminslash,adminfile,webfile,bypass,health,api,fallback,whichspa,webindex decision;
class serveadmin,serveweb,route,healthresp,ctrl,adminindex,serveindex,placeholder,redirect serve;
class blocked,notfound bad;
```
Text alternative: static mounts are checked first and short-circuit when a file exists; everything else passes the availability gate, then routes to the health endpoint, a controller, a 404 for missing assets, or one of the two SPA fallbacks — with a built-in placeholder when the website's index is absent.
**Key property**: static files short-circuit before the availability gate. A deliberately disabled instance therefore still serves the customer's website while blocking `/api/v1` and the admin SPA's routes. This is pre-existing behaviour, retained deliberately (see `architecture.md`).
---
## 3. Admin Bypass Evaluation (FR-24)
The gate's admin bypass currently parses the bearer token **without verifying its signature**, so a forged token grants bypass. Per Q1 = A the middleware will validate the token itself using the same `TokenValidationParameters` as the JWT bearer scheme, obtained from a **single shared source** rather than copied.
This approach was chosen over moving `UseAuthentication()` earlier, which would have changed the pipeline for every module including future ones.
```mermaid
sequenceDiagram
box rgba(246,224,94,0.4) Caller
participant C as Client
end
box rgba(144,205,244,0.4) Gate
participant M as AvailabilityMiddleware
participant V as Token validator
end
box rgba(154,230,180,0.4) Downstream
participant N as Rest of pipeline
end
C->>M: Request with Authorization header
M->>M: check bypass prefixes
M->>V: validate token with shared parameters
alt token valid and role is Owner or Administrator
V-->>M: validated principal
M->>N: continue, gate bypassed
else token invalid, forged, or expired
V-->>M: validation failed
M->>M: evaluate master gate and local status
M-->>C: 503 if disabled, otherwise continue
end
```
Text alternative: the middleware validates the bearer token with the same parameters as the bearer scheme; only a genuinely valid Owner or Administrator token bypasses the gate, while a forged or expired token falls through to normal availability evaluation.
**Behaviour preserved**: an administrator with a valid token can always reach a disabled instance to switch it back on. **Behaviour removed**: an unauthenticated caller can no longer bypass the gate with a self-made token.
---
## 4. Health Reporting
`/health` reports **infrastructure liveness only**. It performs no database call and probes no dependency (D-21).
Per Q4 = C the response is JSON containing status, timestamp, application version and loaded module names.
```mermaid
graph TD
call["GET /health"]
bypasslist["On the availability bypass list
so a disabled instance still answers"]
inproc["Read in-process state only
no database, no dependency probe"]
compose["Compose report:
status, timestamp, version, modules"]
ok["200 Healthy"]
dead["Process not running:
no response at all"]
call --> bypasslist
bypasslist --> inproc
inproc --> compose
compose --> ok
call -.->|"if startup failed"| dead
classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
classDef step fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
classDef good fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef bad fill:#fbb6ce,stroke:#b83280,stroke-width:1px,color:#000;
class call entry;
class bypasslist,inproc,compose step;
class ok good;
class dead bad;
```
Text alternative: the health endpoint answers from in-process state only and stays reachable while the instance is disabled; the failure signal is the absence of a response when the process did not start.
**Why the module list is included**: `ModuleOrchestrator` logs rather than throws when a module fails to load, so an instance can start successfully with reduced capability. Without this field there is no way to detect that after a deploy without host access — which NFR-06 explicitly requires.
**Where the "unhealthy" signal comes from**: not from this endpoint reporting failure, but from the process not answering at all. U2's fail-fast startup is what produces that signal. A liveness check whose process always starts would be worthless; combined with fail-fast migration it is meaningful.
---
## 5. Missing Website Directory
A fresh deployment has no `wwwroot/web/` — the customer's website is deployed separately. The CMS must still start and serve `/admin` and `/api/v1` (Q2 = B, Q3 = A).
```mermaid
graph TD
boot["Startup"]
check{"wwwroot/web exists ?"}
warn["Log a warning with the expected path"]
normal["Register mount normally"]
reg["Register mount tolerating absence"]
run["Application starts either way"]
visit["Visitor requests /"]
hasindex{"index.html present ?"}
site["Serve the website"]
ph["Serve built-in placeholder
explaining no site is deployed
and linking to /admin"]
boot --> check
check -->|no| warn
warn --> reg
check -->|yes| normal
reg --> run
normal --> run
run --> visit
visit --> hasindex
hasindex -->|yes| site
hasindex -->|no| ph
classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
classDef step fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef warnnode fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
class boot,visit entry;
class check,hasindex decision;
class normal,reg,run,site,ph step;
class warn warnnode;
```
Text alternative: a missing website directory logs a warning but never blocks startup; visitors then receive a built-in placeholder page instead of an error, and the admin SPA and API remain fully available.
**Why a warning rather than an informational line** (Q3 = A): on a fresh install the absence is normal, but on a running production instance it means the customer's website has vanished — the exact scenario the release design exists to prevent. A warning is visible in Sentry without blocking startup, so the normal case costs nothing while the dangerous case is not silent.
---
## 6. Trailing Slash for `/admin`
The admin SPA is built with `base: '/admin/'`. A request to `/admin` without the trailing slash resolves relative references one level too high, breaking asset loading in a way that looks like a deployment fault. Per Q5 = A, `/admin` redirects to `/admin/`.
The redirect applies **only** to the exact path `/admin`. Deeper paths such as `/admin/dashboard` are handled by the SPA fallback unchanged.