# Services — Master CMS Module > ⚠️ **Superseded, found stale 2026-07-04**: this document is the *inception-stage* design and describes an earlier pull-based `IMasterAvailabilityService` (static cache + `MasterModuleOptions.CacheMinutes` + `/api/internal/master/*` routes) that was **not what got built**. Construction pivoted to a push-based protocol instead (`/api/v1/master/*` routes, `_masterIsAvailable`/`_masterDisableMessage` fields, no `CacheMinutes`). A slave-pull mechanism was eventually added too, but on 2026-07-04 and with a different shape (`MasterStatusPollingBackgroundService` polling `GET /api/v1/SlaveStatus` on an interval, with a time-based fail-open) than what's described below. **Treat this file as historical intent, not current truth** — the accurate, as-built design lives in `construction/master-backend/functional-design/*.md` and `construction/slave-availability-extension/functional-design/*.md`. This divergence predates 2026-07-04 and was found (not caused) during today's documentation audit; `CacheMinutes`/`ApiKey` dead-config fallout is already tracked as TD-001 in `tech-debt-backlog`. ## Service Definitions ### Master-Side Services #### CmsInstanceService - **Interface**: `ICmsInstanceService` - **Lifetime**: Scoped - **Injected Dependencies**: `ICmsInstanceRepository`, `ISlaveApiClient`, `IOptions`, `ILogger` - **Responsibilities**: Business orchestration — coordinates repository (data) and SlaveApiClient (HTTP side-effects); enforces business rules (mandatory DisableMessage, Inactive cannot be pushed) - **Key orchestration**: On `AddAsync` → persist entity then call `RegisterMasterAsync`; on `UpdateStatusAsync` → validate, persist, then call `PushStatusAsync`; on `VerifyIntegrityAsync` → query all active instances, call `GetRegisteredMasterUrlAsync` per instance, re-register on mismatch #### SlaveApiClient - **Interface**: `ISlaveApiClient` - **Lifetime**: Transient (managed by `IHttpClientFactory`) - **Registration**: `services.AddHttpClient()` - **Injected Dependencies**: `HttpClient` (injected by framework) - **Responsibilities**: Typed HTTP client for all Master → Slave API calls; sets `X-Master-Api-Key` header per call; deserializes responses; returns success flags rather than throwing (callers decide error handling) - **Endpoints called**: - `POST {slaveUrl}/api/internal/master/register` - `PUT {slaveUrl}/api/v1/Availability/admin/status` (reuses existing slave endpoint) - `GET {slaveUrl}/api/internal/master/registration` (integrity check) #### IntegrityCheckBackgroundService - **Lifetime**: Singleton (registered via `services.AddHostedService()`) - **Injected Dependencies**: `IServiceScopeFactory`, `IOptions`, `ILogger` - **Responsibilities**: Periodic background loop; resolves `ICmsInstanceService` via `IServiceScopeFactory` per tick (required because `ICmsInstanceService` is Scoped); runs `VerifyIntegrityAsync()`; interval from `MasterModuleOptions.IntegrityCheckIntervalMinutes` --- ### Slave-Side Services #### MasterAvailabilityService - **Interface**: `IMasterAvailabilityService` - **Lifetime**: Scoped - **Injected Dependencies**: `AvailabilityDbContext`, `IHttpClientFactory`, `IOptions`, `ILogger` - **Responsibilities**: Checks DB for `MasterRegistration`; if none exists returns `HasMaster = false`; if exists checks cache freshness against `MasterModuleOptions.CacheMinutes`; pulls from Master via HTTP on cache miss; returns cached value on HTTP failure (fail-open, NFR-MASTER-01) - **Cache fields** (static): `_cachedStatus` (default `Available`), `_cachedDisableMessage`, `_lastFetchedAt`, `_cachedMasterUrl` - **Exemption**: No special logic needed — a Master CMS instance never calls `RegisterMaster` on itself, so `MasterRegistrations` table is empty → `HasMaster = false` always on a Master instance (FR-MASTER-09) --- ## Orchestration Flows ### Flow 1 — Add Slave CMS ```mermaid sequenceDiagram box rgba(99,179,237,0.3) Frontend participant UI as Browser end box rgba(154,230,180,0.3) Master CMS participant Ctrl as CmsInstanceController participant Svc as CmsInstanceService participant Repo as CmsInstanceRepository participant Client as SlaveApiClient end box rgba(246,224,94,0.3) Slave CMS participant SlaveEndpoint as AvailabilityController end UI->>Ctrl: POST /api/v1/CmsInstances Ctrl->>Svc: AddAsync(request) Svc->>Repo: AddAsync(entity) Repo-->>Svc: entity tracked Svc->>Repo: SaveChangesAsync() Svc->>Client: RegisterMasterAsync(slaveUrl, apiKey, masterUrl) Client->>SlaveEndpoint: POST /api/internal/master/register SlaveEndpoint-->>Client: 200 OK Client-->>Svc: true Svc->>Repo: UpdateAsync (LastContactedAt) Repo-->>Svc: updated Svc->>Repo: SaveChangesAsync() Svc-->>Ctrl: CmsInstanceDto Ctrl-->>UI: 201 Created ``` Text alternative: Browser posts new slave to master controller → service persists → calls slave registration endpoint → updates LastContactedAt → returns DTO. --- ### Flow 2 — Set Slave Status ```mermaid sequenceDiagram box rgba(99,179,237,0.3) Frontend participant UI as Browser end box rgba(154,230,180,0.3) Master CMS participant Ctrl as CmsInstanceController participant Svc as CmsInstanceService participant Repo as CmsInstanceRepository participant Client as SlaveApiClient end box rgba(246,224,94,0.3) Slave CMS participant SlaveCtrl as AvailabilityController end UI->>Ctrl: PUT /api/v1/CmsInstances/{id}/status Ctrl->>Svc: UpdateStatusAsync(id, status, disableMessage) Svc->>Repo: GetByIdAsync(id) Repo-->>Svc: CmsInstance Note over Svc: Validate DisableMessage required if NotAvailable Svc->>Repo: UpdateAsync (Status, DisableMessage) Svc->>Repo: SaveChangesAsync() Svc->>Client: PushStatusAsync(url, apiKey, status, disableMessage) Client->>SlaveCtrl: PUT /api/v1/Availability/admin/status SlaveCtrl-->>Client: 200 OK Client-->>Svc: true Svc->>Repo: UpdateAsync (LastStatusPushedAt) Svc->>Repo: SaveChangesAsync() Svc-->>Ctrl: void Ctrl-->>UI: 200 OK ``` Text alternative: Browser sends status update → master validates, persists, pushes to slave endpoint → updates LastStatusPushedAt → returns 200. --- ### Flow 3 — Integrity Check (Background) ```mermaid sequenceDiagram box rgba(200,200,200,0.3) Background participant Timer as PeriodicTimer participant BgSvc as IntegrityCheckBackgroundService end box rgba(154,230,180,0.3) Master CMS participant Svc as CmsInstanceService participant Repo as CmsInstanceRepository participant Client as SlaveApiClient end box rgba(246,224,94,0.3) Slave CMS participant SlaveEndpoint as Slave API end Timer->>BgSvc: Tick (every IntegrityCheckIntervalMinutes) BgSvc->>Svc: VerifyIntegrityAsync() Svc->>Repo: GetActiveAsync() Repo-->>Svc: list of active CmsInstances loop for each active instance Svc->>Client: GetRegisteredMasterUrlAsync(slaveUrl, apiKey) Client->>SlaveEndpoint: GET /api/internal/master/registration SlaveEndpoint-->>Client: registeredMasterUrl Client-->>Svc: registeredMasterUrl alt URL mismatch Svc->>Client: RegisterMasterAsync(slaveUrl, apiKey, masterUrl) Client->>SlaveEndpoint: POST /api/internal/master/register SlaveEndpoint-->>Client: 200 OK Client-->>Svc: true end end Svc-->>BgSvc: done ``` Text alternative: Background timer triggers integrity service → per active slave checks registered URL → re-registers if mismatch. --- ### Flow 4 — Two-Phase Availability Gate (Slave Middleware) ```mermaid flowchart TD Start(["Incoming HTTP Request"]) Bypass{"Bypass prefix?"} HasMaster{"MasterRegistration\nexists in DB?"} CacheFresh{"Cache fresh?"} PullMaster["Pull status from Master\n(HTTP GET)"] PullOk{"Pull successful?"} MasterStatus{"Master status?"} LocalCheck["Existing local gate\nIAvailabilityService.IsAvailableAsync()"] AdminBypass{"Admin bypass\n(Owner/Admin JWT)?"} Block503Master["503 Service Unavailable\n+ DisableMessage"] Block503Local["503 / Maintenance"] Pass(["Pass request to next middleware"]) Start --> Bypass Bypass -->|yes| Pass Bypass -->|no| HasMaster HasMaster -->|no| LocalCheck HasMaster -->|yes| CacheFresh CacheFresh -->|yes| MasterStatus CacheFresh -->|no| PullMaster PullMaster --> PullOk PullOk -->|yes| MasterStatus PullOk -->|no - use cached| MasterStatus MasterStatus -->|NotAvailable| Block503Master MasterStatus -->|Available| LocalCheck LocalCheck -->|Available| Pass LocalCheck -->|NotAvailable or Maintenance| AdminBypass AdminBypass -->|yes| Pass AdminBypass -->|no| Block503Local classDef decision fill:#FFC107,stroke:#F57F17,stroke-width:2px,color:#000 classDef action fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000 classDef terminal fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000 classDef block fill:#FC8181,stroke:#C53030,stroke-width:2px,color:#000 class Bypass,HasMaster,CacheFresh,PullOk,MasterStatus,AdminBypass decision class PullMaster,LocalCheck action class Start terminal class Pass terminal class Block503Master,Block503Local block ``` Text alternative: Request enters middleware → check bypass → if master registered check cached/pulled status → if NotAvailable return 503 with DisableMessage → else proceed to existing local availability gate. --- ### Flow 5 — Slave Registration (Master Registers Itself) ```mermaid sequenceDiagram box rgba(154,230,180,0.3) Master CMS participant Client as SlaveApiClient end box rgba(246,224,94,0.3) Slave CMS participant Ctrl as AvailabilityController participant DB as AvailabilityDbContext end Client->>Ctrl: POST /api/internal/master/register\nHeader: X-Master-Api-Key\nBody: {masterUrl} Ctrl->>Ctrl: Validate API key vs MasterModuleOptions.ApiKey alt Invalid key Ctrl-->>Client: 401 Unauthorized else Valid key Ctrl->>DB: Upsert MasterRegistration (masterUrl) DB-->>Ctrl: saved Ctrl-->>Client: 200 OK end ``` Text alternative: Master posts registration with API key header → slave validates key → upserts MasterRegistration record → returns 200.