# Domain Entities — U2 Data Durability U2 adds **one** persisted entity and **one** migration. Everything else in the unit is configuration and startup behaviour. --- ## Entity Relationships ```mermaid graph TD appctx["ApplicationDbContext
implements IDataProtectionKeyContext"] key["DataProtectionKey
NEW"] user["ApplicationUser
existing"] refresh["RefreshToken
existing"] invite["Invitation
existing"] avail["GlobalAvailabilityState
existing"] protector["IDataProtector
derived from keys"] cmsinst["CmsInstance
MasterDbContext"] mastreg["MasterRegistration
AvailabilityDbContext"] appctx -->|"owns"| key appctx -->|"owns"| user appctx -->|"owns"| refresh appctx -->|"owns"| invite appctx -->|"owns"| avail key -->|"derives"| protector protector -->|"encrypts API key of"| cmsinst protector -->|"encrypts API key of"| mastreg classDef ctx fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000; classDef newent fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000; classDef existing fill:#e2e8f0,stroke:#4a5568,stroke-width:1px,color:#000; classDef derived fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000; classDef consumer fill:#d6bcfa,stroke:#6b46c1,stroke-width:1px,color:#000; class appctx ctx; class key newent; class user,refresh,invite,avail existing; class protector derived; class cmsinst,mastreg consumer; ``` Text alternative: the Core context gains a Data Protection keys table alongside its existing Identity entities; those keys derive the protector that encrypts the API keys stored on CMS instances and master registrations in the two module contexts. **Cross-context dependency worth noting**: the keys live in `ApplicationDbContext`, while the values they protect live in `MasterDbContext` and `AvailabilityDbContext`. There is no foreign key between them — all three contexts share one database, but the relationship is behavioural, not relational. Losing the keys does not produce a referential-integrity error; it produces rows whose encrypted column can no longer be read. That is exactly why the failure is silent. --- ## DataProtectionKey (new) Provided by the framework via `IDataProtectionKeyContext`; the schema is not authored by this project. | Field | Type | Purpose | |---|---|---| | `Id` | int, identity | Primary key | | `FriendlyName` | string, nullable | Human-readable key identifier | | `Xml` | string | The serialized key material | ### Constraints and rules | Aspect | Rule | |---|---| | Owning context | `ApplicationDbContext` (Q7 of Application Design = A) | | Migration | One new Core migration, applied automatically at startup by FR-11 | | Encryption at rest | **None** — see DEV-05. `Xml` contains usable key material in plain text | | Retention | Rows are **never** deleted (BR-U2-06) | | Rotation | Framework default, 90 days; new rows are added, old rows retained | | Access | Only through the Data Protection API. No application code reads or writes this table directly | **Why `Xml` being plaintext matters**: anyone who can read this table can decrypt every stored slave API key. This is the substance of DEV-05, and why BR-U2-08 requires TLS on the connection and a database that is not publicly reachable. Those compensating controls are not optional extras — they are what makes the deviation acceptable. --- ## ApplicationDbContext (modified) | Change | Detail | |---|---| | Interface | Implements `IDataProtectionKeyContext` | | New set | `DbSet DataProtectionKeys` | | Existing sets | Unchanged — Identity, `RefreshToken`, `Invitation`, `ModulePermission`, `GlobalAvailabilityState` | | Migration behaviour | **Changed**: now migrates automatically at startup (FR-11). Previously required a manual `dotnet ef database update` | **Note on the behaviour change**: automatic migration is a genuine change in operational semantics, not merely a convenience. Previously a schema change reached production only when a human ran a command; now it happens whenever a new release starts. This is why forward-compatible, non-destructive migrations (BR-U2-16) and a pre-deploy backup (FR-20) are load-bearing rather than nice to have. --- ## Configuration Values U2 introduces **no new `appsettings` section**. | Value | Source | Rationale | |---|---|---| | Application discriminator | **Constant in code** | Q1 = A. Cannot be misconfigured, forgotten, or made to differ between instances sharing a database | | Key lifetime | Framework default (90 days) | Q5 = C. No reason to differ | | Migration retry attempts and delays | Constants in code | Values chosen to cover a host-reboot window; not an operational tuning knob | | Connection string | Existing `ConnectionStrings:DefaultConnection` | Unchanged. BR-U2-08 requires TLS to be enforced in it | **Why nothing is configurable here**: every value in this unit exists to prevent a silent failure. A configuration surface would be a way to reintroduce that failure — a discriminator set wrong on one instance, or a key lifetime set so short that rotation outpaces retention. --- ## Persistence Summary | Question | Answer | |---|---| | New tables? | One — the Data Protection keys table | | New migrations? | One, in `SlpModularCms.Core` | | Modified entities? | None. `ApplicationDbContext` gains a set but no existing entity changes | | Destructive schema changes? | None. Purely additive, so rollback by redeploying an earlier release stays safe | | New configuration? | None | --- ## Verification Targets What this unit's tests must actually prove, given that the failure mode is silent: | Target | Why it needs asserting | |---|---| | The persistent key store survives module registration | Registration alone passes in both the broken and fixed cases — only the resulting configuration distinguishes them | | The application discriminator is the fixed constant | The default would change per release directory, defeating persistence | | A protected value round-trips across a simulated content-root change | This is the actual user-visible property: an API key encrypted before a deploy is still readable after it | | Neither module registers Data Protection | Prevents the conflict from being reintroduced by a future change to either module | | A connection failure retries; a migration failure does not | The two paths differ deliberately (BR-U2-11, BR-U2-12) | | Both hosts still start | `SlpModularCms.Api.Slave` has no test project and is a reference instance (Q2 of Application Design = A) |