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