Business Rules — Unit 1: master-backend
BR-01 — Status Update Decision Logic
Text alternative: Load entity (404 if missing) → validate DisableMessage required for NotAvailable → for Inactive, persist the Inactive status and then explicitly push Available (disableMessage=null) to the slave to release the master gate (the master no longer manages this slave, so it must not leave the slave stuck on a stale status) → for other statuses persist, decrypt key, push to slave, set SlaveContactSuccess based on push result.
Updated 2026-07-04: originally Inactive skipped the HTTP push entirely (see history below); this was found to leave slaves permanently stuck on their last pushed status (e.g. NotAvailable) after being detached from the master, and was fixed to always release the gate on deactivation.
BR-02 — Integrity Check Decision Logic
Text alternative: For each active slave — attempt to get its registered master URL; if unreachable set failure flag and move on; if reachable and URL matches clear the flag, if mismatch re-register (clearing the flag on success, setting it on failure); then — regardless of the URL check's outcome, as long as the slave was reachable — re-push the master's currently persisted Status/DisableMessage to that slave (this is the reconciliation path for a slave that reset its in-memory gate, e.g. after a restart, or missed an earlier push). IntegrityCheckBackgroundService also now runs one tick immediately on host startup, in addition to its periodic interval, so this resync happens right after the master (re)starts rather than waiting a full cycle.
Updated 2026-07-04: originally this check only verified/re-registered the master URL and never re-pushed status (see history below); this left restarted slaves stuck on a stale in-memory status (reset to Available by default) until the next explicit status change — fixed by adding the re-push step and the immediate startup run.
Validation Rules
| Rule |
Field |
Condition |
Error |
| BR-VAL-01 |
Name |
Must not be null or whitespace |
Name is required |
| BR-VAL-02 |
Url |
Must not be null or whitespace |
Url is required |
| BR-VAL-03 |
Url |
Must start with http:// or https:// |
Url must be a valid absolute HTTP(S) URL |
| BR-VAL-04 |
ApiKey |
Must not be null or whitespace (on create) |
ApiKey is required |
| BR-VAL-05 |
DisableMessage |
Required (non-empty) when Status = NotAvailable |
DisableMessage is required when status is NotAvailable |
| BR-VAL-06 |
Status |
Must be a valid CmsInstanceStatus enum value |
Invalid status value |
| BR-VAL-07 |
id (update) |
CmsInstance with given id must exist |
CmsInstance not found (404) |
Status Transition Rules
| From |
To |
DisableMessage |
HTTP Push |
Notes |
| Any |
Available |
Clear to null |
Yes |
Slave re-enabled |
| Any |
NotAvailable |
Required, non-empty |
Yes |
Slave disabled with message |
| Any |
Inactive |
Clear to null |
Yes — pushes Available/null |
Master stops managing the slave, but must first release the gate so the slave doesn't stay stuck on its last pushed status |
Inactive |
Available |
Clear to null |
Yes |
Reactivation |
Inactive |
NotAvailable |
Required, non-empty |
Yes |
Reactivation with disable |
Updated 2026-07-04: the Inactive row previously said "No" HTTP push (see history below) — corrected after the no-push behavior was found to leave slaves permanently stuck on their last status.
ApiKey Encryption Rules
| Rule |
Description |
| BR-ENC-01 |
ApiKey is encrypted via IDataProtector before writing to MasterDbContext |
| BR-ENC-02 |
ApiKey is decrypted via IDataProtector immediately before each HTTP call requiring it |
| BR-ENC-03 |
ApiKey is never included in CmsInstanceDto or any other API response |
| BR-ENC-04 |
ApiKey is accepted in CreateCmsInstanceRequest on creation only; no update endpoint for ApiKey |
Master URL Resolution Rules
| Context |
Resolution Strategy |
| Controller-originated calls (Add) |
Derive from HttpContext.Request scheme + host + (optional port) via IHttpContextAccessor |
| Background service calls (Integrity Check) |
Read MasterModuleOptions.MasterUrl from configuration |
MasterModuleOptions.MasterUrl is null in background context |
Log a warning; skip registration/integrity for that cycle |
HTTP Contact Exclusion Rules
| Rule |
Description |
| BR-CONTACT-01 |
Instances with Status = Inactive are excluded from GetActiveAsync and are not contacted by the periodic integrity check / re-push cycle |
| BR-CONTACT-02 |
(Updated 2026-07-04) The transition to Inactive itself always performs exactly one status push — Available, disableMessage=null — to release the master gate on the slave before the instance drops out of GetActiveAsync for good. Previously this push was skipped entirely; that left the slave stuck on its last pushed status indefinitely. |
| BR-CONTACT-03 |
Integrity check (and its new status re-push, see BR-02) runs only against instances where Status != Inactive |
Background Service Rules
| Rule |
Description |
| BR-BG-01 |
IntegrityCheckBackgroundService resolves ICmsInstanceService via IServiceScopeFactory per tick (not injected directly, as service is Scoped) |
| BR-BG-02 |
Each tick creates and disposes its own IServiceScope |
| BR-BG-03 |
Exceptions within a single slave's integrity check are caught, logged, and do not abort processing for remaining slaves |
| BR-BG-04 |
If MasterModuleOptions.MasterUrl is null or empty, the background service logs a warning and skips the entire integrity check for that cycle |
| BR-BG-05 |
(Added 2026-07-04) IntegrityCheckBackgroundService runs one tick immediately on host startup (in addition to its periodic PeriodicTimer cycle), so a freshly (re)started master resyncs slave statuses right away instead of waiting up to IntegrityCheckIntervalMinutes |
Slave Pull (Status Poll) Rules — Added 2026-07-04
This closes a design gap: the original inception requirements (inception/requirements/requirements.md, FR-MASTER-06/07, NFR-MASTER-01) specified a slave-initiated periodic pull with fail-open, but construction implemented push-only. The pull side was added alongside the existing push mechanism (not instead of it) after a slave was observed staying on a stale status through a restart and a deactivation.
| Rule |
Description |
| BR-PULL-01 |
SlaveStatusController exposes GET /api/v1/SlaveStatus, authenticated via the X-Master-Api-Key header — no [Authorize]/JWT, since the caller is a slave process, not a logged-in user |
| BR-PULL-02 |
CmsInstanceService.GetStatusForApiKeyAsync identifies the calling slave by decrypting each active CmsInstance.ApiKey and comparing it to the caller's plain key (no separate slave-identity field exists; the shared key is the only credential) — returns null (→ 401) when no match is found |
| BR-PULL-03 |
A successful poll updates CmsInstance.LastContactedAt, mirroring the existing convention used by push-based master↔slave calls |
| BR-PULL-04 |
/api/v1/SlaveStatus is added to AvailabilityMiddleware's bypass list on the master's own instance, so it stays reachable regardless of the master's own local availability status |