# Business Logic Model — U2 Data Durability
**Unit**: U2 Data Durability
**Requirements**: FR-11, FR-12
---
## 1. Scope of the Logic
U2 delivers nothing a user can see. Its entire value is negative: after this unit, a redeploy **cannot** silently destroy schema state or the trust relationship between a Master and its slaves.
Two mechanisms:
1. **Key ring durability** — Data Protection keys move from the filesystem (discarded by every atomic release switch) into the database, with an application discriminator that does not change when the release directory does
2. **Schema convergence** — `ApplicationDbContext` migrates itself at startup, so a deployment needs no CLI access to the host
Both are startup-time concerns. Neither participates in request handling.
---
## 2. Startup Sequence
```mermaid
graph TD
boot["Host builder starts"]
log["Configure logging and Sentry"]
disc["Discover modules"]
core["AddCoreInfrastructure"]
dp["AddCmsDataProtection
persistent key store plus
fixed application discriminator"]
mods["Module RegisterServices
AddDataProtection removed from both"]
build["Build application"]
mig["Migrate ApplicationDbContext"]
classify{"Failure type ?"}
retry["Retry with backoff"]
fail["Propagate: process does not start"]
usemods["UseModules
module contexts migrate"]
serve["Accept traffic"]
boot --> log
log --> disc
disc --> core
core --> dp
dp --> mods
mods --> build
build --> mig
mig -->|success| usemods
mig -->|failure| classify
classify -->|"connection failure"| retry
classify -->|"migration failure"| fail
retry -->|"attempts remain"| mig
retry -->|"attempts exhausted"| fail
usemods --> serve
classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
classDef step fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef critical fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
classDef bad fill:#fbb6ce,stroke:#b83280,stroke-width:1px,color:#000;
class boot entry;
class log,disc,core,mods,build,usemods,serve step;
class dp,mig,classify,retry critical;
class fail bad;
```
Text alternative: Data Protection is configured with a persistent store before module registration, the Core context migrates before the module contexts, and a migration failure is classified — connection failures are retried with backoff while genuine migration failures stop the process immediately.
---
## 3. The Registration-Order Conflict
This is the unit's most important piece of logic, and it is a *removal* rather than an addition.
`AvailabilityModule.RegisterServices` and `MasterModule.RegisterServices` each call `services.AddDataProtection()` today. Module registration runs **after** the host's registration. In ASP.NET Core, a later `AddDataProtection()` re-registers the configuration chain, so the modules' bare calls would discard the persistent key store configured by the host.
```mermaid
graph TD
subgraph broken["Without the fix"]
h1["Host: AddCmsDataProtection
persistent store configured"]
m1["AvailabilityModule: AddDataProtection"]
m2["MasterModule: AddDataProtection"]
r1["Result: filesystem key ring
FR-12 silently ineffective"]
h1 --> m1
m1 --> m2
m2 --> r1
end
subgraph fixed["With the fix"]
h2["Host: AddCmsDataProtection
persistent store configured"]
m3["Modules: no Data Protection call
they consume IDataProtector only"]
r2["Result: database key ring
survives release switches"]
h2 --> m3
m3 --> r2
end
classDef good fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef bad fill:#fbb6ce,stroke:#b83280,stroke-width:1px,color:#000;
classDef neutral fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
class h1,h2,m3 neutral;
class m1,m2,r1 bad;
class r2 good;
```
Text alternative: leaving the modules' own Data Protection calls in place would override the host's persistent key store and leave the key ring on the filesystem, whereas removing them lets the host's single configuration stand.
**Why this is dangerous rather than merely wrong**: registration tests pass either way — `IDataProtector` resolves in both cases. The defect appears only after the first atomic release switch, as slave API keys that no longer decrypt, presenting as a network fault between Master and slave. The verification for this unit must therefore assert the **resulting configuration**, not merely that Data Protection is registered.
---
## 4. Application Discriminator Stability
The application discriminator determines whether two processes derive the same keys. By default it is derived from the content root path — which changes on every atomic release switch. Persisting keys in the database while letting the discriminator move would produce keys that are stored but unusable: a second, quieter version of the same failure.
Per Q1 = A the discriminator is a **fixed constant in code**.
```mermaid
graph TD
r1["Release directory 1
content root /srv/cms/releases/001"]
r2["Release directory 2
content root /srv/cms/releases/002"]
disc["Fixed discriminator constant"]
keys[("Key ring in database")]
same["Same keys derived
stored values stay readable"]
r1 --> disc
r2 --> disc
disc --> keys
keys --> same
classDef release fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
classDef fixed fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
classDef store fill:#d6bcfa,stroke:#6b46c1,stroke-width:1px,color:#000;
classDef good fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
class r1,r2 release;
class disc fixed;
class keys store;
class same good;
```
Text alternative: two different release directories both use the same fixed discriminator, so the keys stored in the database remain derivable and previously encrypted values stay readable across deploys.
**Why a constant rather than configuration** (Q1 = A): it cannot be set wrong, forgotten during a host migration, or accidentally differ between two instances that share a database. A configurable value would add a way to reintroduce the exact failure this unit exists to prevent.
---
## 5. Migration Failure Classification
Per Q3 = C, failures are classified rather than treated uniformly:
| Failure kind | Meaning | Response |
|---|---|---|
| **Connection failure** | The database is not reachable yet — typically the app and SQL Server starting together after a host reboot | Retry with increasing delay, then fail |
| **Migration failure** | A migration is invalid, conflicts, or cannot be applied | Fail immediately, no retry |
Retrying a genuine migration failure would only delay the inevitable while making the log harder to read. Failing instantly on a transient connection error would make a host reboot look like a broken deployment.
**Interaction with U1's health check**: after retries are exhausted the exception propagates and the process does not start. `/health` then does not answer, and UptimeRobot goes red. That chain is the entire reason a liveness-only check is sufficient — it is meaningful precisely because startup is strict.
---
## 6. Migration Ordering
```mermaid
graph TD
corectx["ApplicationDbContext
Identity, refresh tokens,
invitations, Data Protection keys"]
availctx["AvailabilityDbContext
master registration"]
masterctx["MasterDbContext
CMS instances"]
protector["IDataProtector consumers
encrypted API keys"]
corectx -->|"migrates first, at startup"| availctx
corectx -->|"migrates first, at startup"| masterctx
corectx -->|"keys table must exist before"| protector
availctx -->|"uses"| protector
masterctx -->|"uses"| protector
classDef core fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
classDef module fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef consumer fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
class corectx core;
class availctx,masterctx module;
class protector consumer;
```
Text alternative: the Core context migrates first because it now owns the Data Protection keys table, which both module contexts depend on indirectly through their encrypted API key handling.
**Why Core must be first**: the keys table lives in `ApplicationDbContext` (Q7 of Application Design = A). Both modules encrypt and decrypt API keys. If a module migrated and immediately used an `IDataProtector` before the keys table existed, key generation would fail against a missing table.
**Scope boundary**: U2 adds automatic migration for `ApplicationDbContext` only. `AvailabilityDbContext` and `MasterDbContext` already migrate themselves in their `UseModule` implementations, and that existing behaviour is left untouched — changing it would alter module behaviour beyond this feature's scope.
---
## 7. Concurrent Migration
Per Q4 = C, no locking mechanism is built. The design assumption is **one instance per database**, which holds today: the Master and each slave have their own database.
This is documented in the deployment instructions as an operational constraint rather than enforced in code — building a distributed migration lock for a situation that cannot currently occur would add failure modes without removing any.
**Recorded as an assumption**: if the deployment model ever changes to multiple instances sharing one database, automatic startup migration must be revisited before that change is made.