# 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.