diff --git a/product/suite/architecture/evolith-suite-deployment-strategy.es.md b/product/suite/architecture/evolith-suite-deployment-strategy.es.md
index 461b7b54..845ce34a 100644
--- a/product/suite/architecture/evolith-suite-deployment-strategy.es.md
+++ b/product/suite/architecture/evolith-suite-deployment-strategy.es.md
@@ -3,9 +3,11 @@
# Suite Evolith — Estrategia de Despliegue (Kubernetes de Cluster Único)
> **Estado:** Propuesto (consolidado BMAD) · **Owner:** Evolith Architecture Board
-> **Autoridad:** [ADR-0107](../../../reference/core/architecture/adrs/core/0107-single-cluster-kubernetes-deployment-topology.es.md) (topología de cluster único) · [ADR-0106](../../../reference/core/architecture/adrs/core/0106-master-tenant-context-projections.es.md) (proyecciones de tenant) · diseño canónico del flujo: `mms/docs/architecture/tenant-master-data-projection.md`
-> **Método:** producido por un análisis multi-agente BMAD — Winston (Arquitecto), experto DevOps, experto de Infraestructura — anclado en el estado real de los cuatro repos (evolith, mms, ums, evolith_tracker), y verificado adversarialmente por críticos de grounding/completitud/operación. Las correcciones verificadas están incorporadas (ver §5, §15).
-> **Fecha:** 2026-07-09
+> **Autoridad:** [ADR-0107](../../../reference/core/architecture/adrs/core/0107-single-cluster-kubernetes-deployment-topology.es.md) (topología de cluster único) · [ADR-0129](../../../reference/core/architecture/adrs/core/0129-ums-is-the-tenant-master.es.md) (el maestro de Tenant es UMS) · decisiones de los satélites: ADR-UMS-107 (emisor) y T-059 (consumidor)
+> **Método:** producido por un análisis multi-agente BMAD — Winston (Arquitecto), experto DevOps, experto de Infraestructura — anclado en el estado real de los repos, y verificado adversarialmente por críticos de grounding/completitud/operación. Las correcciones verificadas están incorporadas (ver §5, §15).
+> **Fecha:** 2026-07-09 · **Rebasado sobre la suite de tres productos:** 2026-08-23
+
+> **Qué cambió el 2026-08-23.** Este plan se escribió para cuatro productos, y uno de ellos — **MMS** — nunca existió. ADR-0106 lo nombraba maestro de Tenant; ADR-0129 lo supersede, y los dos satélites ya se habían movido: UMS posee y publica el tenant (ADR-UMS-107) y el Tracker lo consume (T-059). Todo lo que tenía forma de MMS sale de este documento: su namespace, su base de datos, su pipeline de CI, sus riesgos, y la migración de ownership M0–M4 que existía para entregarle una autoridad que nunca iba a tener. La §5 de mensajería se redibuja sobre **un** productor y **una** cola consumidora.
---
@@ -24,8 +26,8 @@
| Secretos | K8s Secrets (local) → **OpenBao + ESO** (VPS) → **Azure Key Vault + CSI** (AKS); mismos nombres de Secret para que los charts no cambien | §7 |
| Ingress | **Traefik en todas partes** (los charts de Core+Tracker ya usan IngressRoute; k3s lo trae; el chart de UMS se reconstruye sobre el template set de Tracker) · 1 IP pública + routing por host · cert-manager + Let's Encrypt | §7 |
| Estrategia de despliegue | **RollingUpdate en todo** (`maxSurge:1, maxUnavailable:0` + PDB); sin blue-green/canary hasta tener tráfico real + dashboards SLO | §10 |
-| Contrato | NuGet compartido **`Evolith.Messaging.Contracts`** (id de paquete), el namespace C# se mantiene **`Evolith.Contracts.MasterData`** (MassTransit enruta por namespace+tipo); expand-contract; **un solo major de schema por consumidor** | §11 |
-| Migración de ownership | Cinco fases con gates **M0–M4** (plumb → backfill → freeze writers → switch reads → contract) | §12 |
+| Contrato | Namespace **`Evolith.Contracts.Tenancy`** — un namespace de SUITE, no del emisor, porque MassTransit enruta por namespace+tipo; expand-contract; **un solo major de schema por consumidor**. Hoy ningún paquete lo publica: el tipo está duplicado literalmente en UMS y en el Tracker | §11 |
+| Migración de ownership | **Cerrada el 2026-08-22** por ADR-UMS-107 + T-059 — UMS siempre fue el escritor; el Tracker dejó de autorar lo que UMS posee | §12 |
| Gates de promoción | **Escalera G0–G4**; G3 = la maquinaria de gates de Evolith existente (`evolith-cli gate evaluate -p qa`, gate-F4 "RC Stamped") | §13 |
| Probes | **Readiness NUNCA depende de AMQP** — una caída del broker degrada frescura, no debe drenar la flota HTTP | §5.4 |
@@ -52,24 +54,21 @@
```mermaid
flowchart LR
subgraph MASTER["Autoridad — datos maestros"]
- MMS["MMS
Tenant MASTER writer of record
ADR-0106"]
+ UMS["UMS
Tenant MASTER writer of record
+ identidad y authz
ADR-0129 · ADR-UMS-107"]
end
subgraph PLATFORM["Plataforma compartida — ADR-0107"]
- BUS["RabbitMQ
TenantEvent (envelope CloudEvents)"]
+ BUS["RabbitMQ
TenantSnapshotIntegrationEvent (envelope CloudEvents)"]
OBS["Observabilidad
OTel · Prometheus · Tempo · Grafana"]
end
subgraph CONSUMERS["Consumidores — proyecciones read-only"]
- UMS["UMS
identidad y authz
masterdata.tenant_projection"]
- TRK["Evolith Tracker
gobernanza SDLC
masterdata.tenant_projection"]
+ TRK["Evolith Tracker
gobernanza SDLC
tenants + ums_projection_version"]
end
CORE["Evolith Core
motor de evaluación stateless
solo HTTP — NUNCA en el broker"]
- MMS -- "outbox transaccional (ADR-0033)" --> BUS
- BUS -- "cola ums.tenant-projection
inbox + guarda de secuencia" --> UMS
- BUS -- "cola tracker.tenant-projection
inbox + guarda de secuencia" --> TRK
+ UMS -- "outbox transaccional (ADR-0033)" --> BUS
+ BUS -- "cola tracker.tenant-snapshot
inbox + guarda de versión" --> TRK
UMS -- "HTTP evaluate" --> CORE
TRK -- "HTTP evaluate" --> CORE
- MMS -.-> OBS
UMS -.-> OBS
TRK -.-> OBS
CORE -.-> OBS
@@ -84,8 +83,7 @@ flowchart TB
subgraph NSMSG["ns evolith-messaging"]
RMQ["RabbitmqCluster evolith-rabbitmq
replicas: 1 local+VPS · 3 AKS"]
EX["Type-exchange MassTransit (fanout)
Evolith.Contracts.MasterData:TenantEvent"]
- QU["cola ums.tenant-projection
+ ums.tenant-projection_error"]
- QT["cola tracker.tenant-projection
+ tracker.tenant-projection_error"]
+ QT["cola tracker.tenant-snapshot
+ tracker.tenant-snapshot_error"]
end
subgraph NSOBS["ns observability (BUILD)"]
OTEL["otel-collector"] --- PROM["Prometheus"] --- TEMPO["Tempo"] --- GRAF["Grafana"]
@@ -93,23 +91,20 @@ flowchart TB
subgraph NSCORE["ns evolith-core"]
COREAPI["core-api"] --- MCP["evolith-mcp"] --- AGRT["agent-runtime"]
end
- subgraph NSMMS["ns mms"]
- MMSAPI["mms-api"] --- MMSDB[("Postgres mms
masterdata: tenants, event store, outbox")]
- end
+
subgraph NSUMS["ns ums"]
- UMSAPI["ums-api"] --- UMSWEB["ums-web"] --- UMSDB[("Postgres ums
+ masterdata.tenant_projection + inbox")]
+ UMSAPI["ums-api"] --- UMSWEB["ums-web"] --- UMSDB[("Postgres ums
tenants (maestro) + outbox")]
end
subgraph NSTRK["ns tracker"]
- TRKAPI["tracker-api"] --- TRKWEB["tracker-web"] --- TRKDB[("Postgres tracker
+ masterdata.tenant_projection + inbox")]
+ TRKAPI["tracker-api"] --- TRKWEB["tracker-web"] --- TRKDB[("Postgres tracker
tenants + ums_projection_version + inbox")]
end
end
LB --> UMSWEB & TRKWEB & COREAPI
- MMSAPI -->|"AMQP · usuario mms"| EX
- EX --> QU & QT
- QU -->|"consume · usuario ums"| UMSAPI
+ UMSAPI -->|"AMQP · usuario ums"| EX
+ EX --> QT
QT -->|"consume · usuario tracker"| TRKAPI
UMSAPI & TRKAPI -->|"HTTP core-api.evolith-core.svc"| COREAPI
- MMSAPI & UMSAPI & TRKAPI -.->|OTLP| OTEL
+ UMSAPI & TRKAPI -.->|OTLP| OTEL
RMQ -.->|":15692"| PROM
```
@@ -126,7 +121,7 @@ Multi-cluster-por-producto se **rechaza**: obliga a federar/shovel RabbitMQ entr
| Nodos | 1 (`deploy/kubernetes/kind-cluster.yaml`) | 1 | 1–2 (limitado por RAM: clase 7.8 GB) | system 2×B2s + user 3×D4as_v5 en 3 AZs, autoscaler 3→6 |
| CNI | **Cilium** (instalar con `disableDefaultCNI: true` — kindnet NO aplica NetworkPolicy; paridad con AKS) | default k3s o Cilium | idem | Azure CNI Overlay + dataplane **Cilium** |
| RabbitMQ | replicas **1** (overlay de values) | replicas 1, PV 5Gi | replicas **1** (3 réplicas en un nodo es HA falsa y triplica RAM; quorum=3 solo con ≥3 nodos) | replicas 3, anti-affinity de zona, PVs ZRS |
-| Postgres | StatefulSet in-cluster por producto | **CloudNativePG** por producto + backups base/WAL fuera del nodo (S3-compatible) | idem | **Azure Database for PostgreSQL Flexible Server** por producto (zone-redundant para MMS — autoridad maestra; burstable para proyecciones) |
+| Postgres | StatefulSet in-cluster por producto | **CloudNativePG** por producto + backups base/WAL fuera del nodo (S3-compatible) | idem | **Azure Database for PostgreSQL Flexible Server** por producto (zone-redundant para UMS — autoridad maestra; burstable para la proyección del Tracker) |
| Storage class | default kind | local-path | local-path + backups off-node obligatorios | managed-csi / premium+ZRS para broker y BD |
| TLS | ninguno/mkcert | cert-manager + issuer LE staging | cert-manager + LE prod | cert-manager + LE prod |
| Secretos | K8s Secrets planos | OpenBao + ESO | OpenBao + ESO (GT-112) | Azure Key Vault + CSI + workload identity |
@@ -135,22 +130,21 @@ Multi-cluster-por-producto se **rechaza**: obliga a federar/shovel RabbitMQ entr
### 4.2 Postura de HA por ambiente (explícita)
-- **prod-VPS: sin HA por diseño.** Disponibilidad = restauración rápida: CNPG PITR + WAL off-node, broker quorum-de-1 en PVs durables, RTO documentado (≤30 min) / RPO (≤5 min vía WAL). El **outbox** de MMS hace que la caída del broker sea sin pérdida para el productor; los consumidores se ponen al día. **Disparador de HA real:** ≥3 nodos → broker replicas 3 + réplicas CNPG.
-- **prod-AKS: el tier de HA real.** Nodos en 3 AZs, broker quorum 3 con anti-affinity de zona, Postgres de MMS zone-redundant.
+- **prod-VPS: sin HA por diseño.** Disponibilidad = restauración rápida: CNPG PITR + WAL off-node, broker quorum-de-1 en PVs durables, RTO documentado (≤30 min) / RPO (≤5 min vía WAL). El **outbox** de UMS hace que la caída del broker sea sin pérdida para el productor; el consumidor se pone al día. **Disparador de HA real:** ≥3 nodos → broker replicas 3 + réplicas CNPG.
+- **prod-AKS: el tier de HA real.** Nodos en 3 AZs, broker quorum 3 con anti-affinity de zona, Postgres de UMS zone-redundant.
### 4.3 Dimensionamiento (requests/limits — derivar de aquí los ResourceQuota por namespace)
| Componente | requests (VPS) | limits (VPS) | AKS |
|---|---|---|---|
-| mms-api | 100m / 128Mi | 500m / 384Mi | 250m/256Mi → 1/512Mi |
| ums-api | 150m / 256Mi | 750m / 768Mi | 500m/512Mi → 1/1Gi |
| ums-web / tracker-web (nginx) | 25m / 32Mi | 100m / 64Mi | idem |
| tracker-api | 150m / 256Mi | 750m / 768Mi | 500m/512Mi → 1/1Gi |
| core-api + mcp + agent-runtime (c/u) | 100m / 192Mi | 500m / 512Mi | 250m/256Mi → 1/768Mi |
-| Postgres ×3 (in-cluster) | 100m / 256Mi c/u | 500m / 512Mi c/u | gestionado (n/a) |
+| Postgres ×2 (in-cluster) | 100m / 256Mi c/u | 500m / 512Mi c/u | gestionado (n/a) |
| RabbitMQ (replicas 1) | 250m / 512Mi | 1 / 1Gi | ×3 @ 500m/1Gi → 1/2Gi |
| Observabilidad (perfil mínimo) | 300m / 1Gi total | 1 / 2Gi total | perfil completo 2 / 4Gi |
-| **Total (VPS, requests)** | **≈1.6 vCPU / ≈3.6 GiB** | cabe en 2 vCPU / 7.8 GB con margen | — |
+| **Total (VPS, requests)** | **≈1.4 vCPU / ≈3.1 GiB** | cabe en 2 vCPU / 7.8 GB con margen | — |
---
@@ -159,31 +153,33 @@ Multi-cluster-por-producto se **rechaza**: obliga a federar/shovel RabbitMQ entr
La verificación adversarial encontró que la topología CRD previamente declarada **no coincide con cómo MassTransit mueve mensajes en realidad**. Tres defectos verificados y sus resoluciones:
### 5.1 `x-consistent-hash` no hace fan-out (crítico, corregido por cambio de diseño)
-Un exchange consistent-hash enruta cada mensaje a **exactamente una** cola enlazada — con `ums.tenant-projection` y `tracker.tenant-projection` ambas enlazadas, cada evento llegaría a UMS **o** a Tracker (~50/50 por hash de tenantId), nunca a ambos. **Regla:** consistent-hash es una *herramienta de particionamiento dentro de un grupo consumidor*, nunca de distribución pub/sub. El fan-out requiere un exchange fanout/topic con un binding por grupo consumidor.
+Un exchange consistent-hash enruta cada mensaje a **exactamente una** cola enlazada. Con dos colas consumidoras enlazadas —la forma que este plan asumía— cada evento habría llegado a **una** o a **la otra** (~50/50 por hash de tenantId), nunca a ambas. Hoy la suite tiene un solo consumidor (`tracker.tenant-snapshot`), así que el reparto todavía no puede morder, y por eso justamente la regla se deja escrita en vez de borrarla: mordería en silencio el día que se enlace un segundo grupo consumidor. **Regla:** consistent-hash es una *herramienta de particionamiento dentro de un grupo consumidor*, nunca de distribución pub/sub. El fan-out requiere un exchange fanout/topic con un binding por grupo consumidor.
### 5.2 MassTransit es dueño de la topología de mensajes (crítico, decisión)
-MassTransit auto-declara un **type-exchange fanout** (`Evolith.Contracts.MasterData:TenantEvent`) y enlaza el exchange/cola de cada endpoint consumidor — esa es la topología por la que fluyó el E2E validado; el exchange CRD era letra muerta, y las colas pre-creadas por CRD con argumentos DLX harían fallar la re-declaración de MassTransit (`406 PRECONDITION_FAILED` → el endpoint falla para siempre mientras el pod sigue Ready — la clásica falla silenciosa de las 3 a.m.).
+MassTransit auto-declara un **type-exchange fanout** (`Evolith.Contracts.Tenancy:TenantSnapshotIntegrationEvent`) y enlaza el exchange/cola de cada endpoint consumidor — esa es la topología por la que fluyó el E2E validado; el exchange CRD era letra muerta, y las colas pre-creadas por CRD con argumentos DLX harían fallar la re-declaración de MassTransit (`406 PRECONDITION_FAILED` → el endpoint falla para siempre mientras el pod sigue Ready — la clásica falla silenciosa de las 3 a.m.).
**Decisión — adoptar las convenciones de MassTransit:**
- **Retirar** las CRDs `Exchange`/`Queue`/`Binding` de `deploy/kubernetes/messaging/tenant-topology.yaml` para la ruta de mensajes.
- **Mantener** CRDs del Topology Operator para lo que MassTransit no puede declarar: **`User`/`Permission`** por producto (y `Policy` opcional).
-- Los nombres de endpoint quedan fijados en código (`ums.tenant-projection`, `tracker.tenant-projection`).
-- Permisos del broker como **regex sobre prefijos de nombres** (grants solo por verbo rompen el arranque de MassTransit): `mms` → configure/write sobre `^(Evolith\.Contracts\.MasterData.*|mms\..*)$`; `ums` → configure/write/read sobre `^(ums\..*|Evolith\.Contracts\.MasterData.*)$`; `tracker` simétrico.
+- El nombre del endpoint consumidor queda fijado en código (`tracker.tenant-snapshot`, en `TenantSnapshotConsumerDefinition`).
+- Permisos del broker como **regex sobre prefijos de nombres** (grants solo por verbo rompen el arranque de MassTransit): `ums` → configure/write sobre `^(Evolith\.Contracts\.Tenancy.*|ums\..*)$`; `tracker` → configure/write/read sobre `^(tracker\..*|Evolith\.Contracts\.Tenancy.*)$`.
### 5.3 Los mensajes veneno caen en `_error`, no en un DLX (mayor, decisión)
-Agotados los reintentos, MassTransit **mueve** el mensaje fallido a `_error` — nunca hace nack, así que el `x-dead-letter-exchange` del broker jamás se dispara. **Decisión:** adoptar la convención de MassTransit — alertar sobre profundidad > 0 de `ums.tenant-projection_error` / `tracker.tenant-projection_error`; el runbook de reproceso hace shovel de `_error` a la cola principal; las CRDs DLX/DLQ se retiran junto con §5.2.
+Agotados los reintentos, MassTransit **mueve** el mensaje fallido a `_error` — nunca hace nack, así que el `x-dead-letter-exchange` del broker jamás se dispara. **Decisión:** adoptar la convención de MassTransit — alertar sobre profundidad > 0 de `tracker.tenant-snapshot_error`; el runbook de reproceso hace shovel de `_error` a la cola principal; las CRDs DLX/DLQ se retiran junto con §5.2.
### 5.4 Regla de probes (contradicción resuelta)
**`/health/ready` verifica solo la BD propia del producto. La conectividad al broker NUNCA condiciona readiness** — los consumers de proyección viven dentro de `ums-api`/`tracker-api`; condicionar readiness a AMQP convertiría cualquier caída del broker en una caída HTTP total de la suite (auth incluida). La salud del broker es una señal separada de modo degradado: métrica + alerta (`bus disconnected`, `projection lag`).
-### 5.5 Fixes P0 de correctitud del consumidor (defectos verificados en el código actual)
-1. **Inbox no cableado realmente:** ambos repos llaman `AddEntityFrameworkOutbox()` a nivel de bus pero las consumer definitions nunca llaman `endpointConfigurator.UseEntityFrameworkOutbox(context)` — `InboxState` existe pero nunca se consulta. Añadirlo en ambas `TenantProjectionConsumerDefinition`.
-2. **Carrera read-check-write:** el upsert versionado no tiene token de concurrencia; dos eventos en vuelo del mismo tenant pueden regresar la proyección permanentemente. Fix con escritura condicional set-based: `INSERT … ON CONFLICT (tenant_id) DO UPDATE SET … WHERE tenant_projection.version < EXCLUDED.version` (lo más barato; además elimina un round-trip).
-3. **Migraciones al arranque compiten con replicas>1** (MigrateAsync en MMS `Program.cs`, migrators de UMS/Tracker): adoptar el patrón **migrate-Job** de Tracker (`evolith_tracker/product/infra/helm/evolith-tracker-api/templates/migrate-job.yaml`) en toda la suite.
-4. **Bug de fallback `Default` vs `DefaultConnection`** (DI de UMS + Tracker): el contexto de proyección apunta silenciosamente a localhost cuando falta `MasterDataDb` — fijar siempre `ConnectionStrings__MasterDataDb` explícito en los charts y corregir el fallback.
+### 5.5 Correctitud del consumidor — tres de cuatro defectos están cerrados
+Esta sección listaba cuatro defectos P0 encontrados en los consumidores de la era MMS. T-059 reconstruyó ese consumidor contra el retrato de UMS y cerró tres; se conservan aquí porque lo que un consumidor futuro tiene que satisfacer son las *razones*, no porque el trabajo siga abierto.
+
+1. ~~**Inbox no cableado realmente**~~ — **cerrado**. `TenantSnapshotConsumerDefinition` llama ahora a `endpointConfigurator.UseEntityFrameworkOutbox(context)` en el endpoint, que es lo que hace que `InboxState` se consulte de verdad; el `AddEntityFrameworkOutbox` a nivel de bus nunca lo hizo. Sobre el mismo contexto donde escribe el consumidor, así que deduplicar y proyectar caen en una sola transacción.
+2. ~~**Carrera read-check-write**~~ — **cerrado**. El upsert es set-based y va guardado: `ON CONFLICT (id) DO UPDATE … WHERE ums_projection_version < EXCLUDED.ums_projection_version`. La guarda cubre lo que el inbox no puede — reentrega tras un reinicio y entrega desordenada, que para un bróker son comportamiento normal.
+3. **Migraciones al arranque compiten con replicas>1** — **sigue abierto** para UMS y el Tracker: adoptar el patrón **migrate-Job** de Tracker (`evolith_tracker/product/infra/helm/evolith-tracker-api/templates/migrate-job.yaml`) en toda la suite.
+4. ~~**Bug de fallback `Default` vs `DefaultConnection`**~~ — **sin objeto**. T-059 retiró la cadena de conexión aparte `MasterDataDb`; la proyección aterriza en el esquema propio del Tracker, así que no queda un segundo contexto que pueda caer a localhost.
### 5.6 Semántica de dependencia
-Productor: el outbox transaccional de MMS (validado en vivo) hace que la caída del broker sea **sin pérdida** — los writes commitean, los eventos drenan al reconectar. Consumidores: quedan idle y se ponen al día. La caída del broker degrada **solo frescura, nunca correctitud**. Sin init-containers de orden, sin esperas al arranque.
+Productor: el outbox transaccional de UMS (validado en vivo) hace que la caída del broker sea **sin pérdida** — los writes commitean, los eventos drenan al reconectar. El consumidor queda idle y se pone al día. La caída del broker degrada **solo frescura, nunca correctitud**. Sin init-containers de orden, sin esperas al arranque.
---
@@ -191,22 +187,22 @@ Productor: el outbox transaccional de MMS (validado en vivo) hace que la caída
| Eje | Decisión |
|---|---|
-| BD | BD-por-producto; schema `masterdata` por repo (master en MMS; proyección en UMS/Tracker) — sin acceso cross-producto, forzado por NetworkPolicy + credenciales distintas |
-| Config | Un ConfigMap por namespace de producto, renderizado por su chart; claves estandarizadas: `DefaultConnection`, `MasterDataDb`, `RabbitMq` |
+| BD | BD-por-producto (UMS guarda el tenant maestro; el Tracker su proyección, en su propio esquema) — sin acceso cross-producto, forzado por NetworkPolicy + credenciales distintas |
+| Config | Un ConfigMap por namespace de producto, renderizado por su chart; claves estandarizadas: `DefaultConnection`, `RabbitMq` |
| Secretos | `-db`, `-broker` por namespace; **usuarios de broker por producto** vía CRDs (default-user compartido rechazado: una fuga = blast radius de toda la suite) |
| Cómputo | ResourceQuota + LimitRange por ns (valores en §4.3); HPA por deployment; PDB `minAvailable:1` donde replicas≥2 |
| Monitoreo | ServiceMonitor + PrometheusRule por producto, **dentro de su chart**, descubiertos por el Prometheus compartido vía label `evolith.dev/product` |
| Logs | stdout JSON → Alloy/Promtail → Loki (label de namespace) |
-| Trazas | OTel SDK → collector compartido → Tempo; el `correlationId` del envelope es la clave de join; UMS debe añadir `AddSource("MassTransit")`; MMS debe propagar el `traceparent` entrante |
+| Trazas | OTel SDK → collector compartido → Tempo; el `correlationId` del envelope es la clave de join; UMS debe añadir `AddSource("MassTransit")` |
| Health | `/health/live` (proceso) + `/health/ready` (solo BD propia — §5.4) |
| Releases | Un release Helm por producto; umbrella chart solo local (ADR-0107 §6) |
## 7. Ingress, redes, DNS, TLS, NetworkPolicies
- **Traefik en todas partes** (los charts de Core + Tracker ya templan `IngressRoute`; la VPS ya corre Traefik bajo Coolify; k3s lo trae — deshabilitar el bundled e instalar el chart pineado). El `httproute.yaml` Gateway-API de UMS (deshabilitado por defecto) se retira al reconstruir el chart de UMS sobre el template set de Tracker. Traefik v3 también implementa Gateway API — ninguna puerta se cierra.
-- **Exposición: 1 IP pública + routing por host** (IPs por servicio rechazadas — costo + sprawl DNS, sin ganancia de aislamiento). Hosts: `mms|ums|tracker|core.`; local `*.evolith.local` en /etc/hosts; staging `*.stg.`; `product/infra/deployment-topology.md` sigue siendo el mapa canónico de nombres.
-- Este-oeste interno: solo ClusterIP + DNS del cluster — broker `evolith-rabbitmq.evolith-messaging.svc:5672`, Core `core-api.evolith-core.svc`. Solo frontends/APIs de usuario reciben IngressRoutes. ⚠️ **El CRUD de tenants de MMS no tiene autenticación hoy — authN es precondición dura para cualquier exposición por ingress de MMS.**
-- **NetworkPolicy: default-deny ingress+egress por namespace de producto**, con allows explícitos: `{mms,ums,tracker}→evolith-messaging:5672` · `{ums,tracker}→evolith-core:HTTP` · `ingress→productos:8080` · `observability→todos:metrics` · `cada producto→su BD:5432` · `todos→kube-dns:53` (+ OTLP 4317, solver de cert-manager). **Regla estructural: `evolith-core` NO tiene ruta al 5672** — "Core nunca en el broker" forzado por la red. El kind local debe correr **Cilium** o todo el modelo queda silenciosamente sin aplicar (§4.1).
+- **Exposición: 1 IP pública + routing por host** (IPs por servicio rechazadas — costo + sprawl DNS, sin ganancia de aislamiento). Hosts: `ums|tracker|core.`; local `*.evolith.local` en /etc/hosts; staging `*.stg.`; `product/infra/deployment-topology.md` sigue siendo el mapa canónico de nombres.
+- Este-oeste interno: solo ClusterIP + DNS del cluster — broker `evolith-rabbitmq.evolith-messaging.svc:5672`, Core `core-api.evolith-core.svc`. Solo frontends/APIs de usuario reciben IngressRoutes.
+- **NetworkPolicy: default-deny ingress+egress por namespace de producto**, con allows explícitos: `{ums,tracker}→evolith-messaging:5672` · `{ums,tracker}→evolith-core:HTTP` · `ingress→productos:8080` · `observability→todos:metrics` · `cada producto→su BD:5432` · `todos→kube-dns:53` (+ OTLP 4317, solver de cert-manager). **Regla estructural: `evolith-core` NO tiene ruta al 5672** — "Core nunca en el broker" forzado por la red. El kind local debe correr **Cilium** o todo el modelo queda silenciosamente sin aplicar (§4.1).
## 8. Persistencia y backups
@@ -214,7 +210,7 @@ Productor: el outbox transaccional de MMS (validado en vivo) hace que la caída
|---|---|---|
| local | StatefulSet por producto (corregir el Postgres `emptyDir` del chart de UMS → PVC) | 1 réplica, PV |
| staging / prod-VPS | **CloudNativePG** por producto; backups base programados + archivado WAL **fuera del nodo** (MinIO/Backblaze). Un StatefulSet sin historia de backup no es producción | 1 réplica, PV durable |
-| prod-AKS | **Flexible Server** por producto (zone-redundant para MMS) | 3 réplicas, ZRS premium |
+| prod-AKS | **Flexible Server** por producto (zone-redundant para UMS) | 3 réplicas, ZRS premium |
## 9. CI/CD y GitOps (Flux CD v2)
@@ -222,7 +218,7 @@ Productor: el outbox transaccional de MMS (validado en vivo) hace que la caída
- **Tags de imagen:** `sha-<7>` inmutable en cada merge a develop **más un tag ordenable `develop--`** — ImagePolicy de Flux no puede ordenar shas puros; la automatización de staging se ancla al patrón con timestamp (`^develop-[a-f0-9]+-(?P[0-9]+)`, numérico asc). Tags de release `X.Y.Z`. Registry: `ghcr.io/beyondnetcode/*`.
- **Charts:** SemVer por chart, publicados como OCI en `ghcr.io/beyondnetcode/charts/`.
- **Staging:** auto-bump por Flux Image Automation (commit de vuelta al git = trazabilidad). **Prod:** chart exacto + imagen exacta pineados vía PR al fleet repo; el PR *es* el evento de promoción; el stamp gate-F4 es status check requerido.
-- **Pipelines por repo (línea base corregida):** UMS **tiene** CI (build/test, SonarCloud, security, release-candidate, contract-validation) y Tracker **tiene** CI (build+test con Postgres real, contract-conformance); **MMS no tiene ninguno**. BUILD: pipeline completo de MMS; jobs de build+push de imagen + publish de chart + Trivy en los cuatro repos; `docker-images.yml` de Core extendido con builds de develop-sha.
+- **Pipelines por repo (línea base corregida):** UMS **tiene** CI (build/test, SonarCloud, security, release-candidate, contract-validation) y Tracker **tiene** CI (build+test con Postgres real, contract-conformance). BUILD: jobs de build+push de imagen + publish de chart + Trivy en los tres repos; `docker-images.yml` de Core extendido con builds de develop-sha.
```
PR ──G0──▶ develop ──▶ GHCR (sha + develop-sha-ts) ──▶ Flux sube staging (auto)
@@ -234,82 +230,85 @@ PR ──G0──▶ develop ──▶ GHCR (sha + develop-sha-ts) ──▶ Flu
| Componente | Estrategia | Notas |
|---|---|---|
-| APIs stateless | RollingUpdate `maxSurge:1,maxUnavailable:0` + PDB | Precondición: endpoints `/health` reales en MMS (hoy los probes van a `/openapi/v1.json`, solo Development → CrashLoop en prod) |
+| APIs stateless | RollingUpdate `maxSurge:1,maxUnavailable:0` + PDB | Toda superficie responde `/health/live` + `/health/ready` (§5.4) |
| Web SPAs | RollingUpdate | El patrón envsubst de tracker-web es la referencia |
| Consumers de proyección | Desplegar libremente — la cola bufferea; **orden/concurrencia resueltos por el upsert condicional de §5.5** | Escalar después con particiones hash por grupo, nunca asumiendo orden entre competing consumers |
| Postgres / RabbitMQ | Gestionados por operator; nunca en pipelines de producto | Cambios de topología solo aditivos |
| Schema de eventos | Expand-contract en el cable (§11) | Consumidores primero para aditivos; dual-publish para breaking |
| Migraciones EF | **Job de migración (hook Helm)** — nunca al arranque | §5.5 |
-| Rollback | `git revert` del pin en el fleet repo → Flux reconcilia el anterior | `helm rollback` = solo break-glass, luego realinear git. **Nunca rollback a través de una migración contract**; restore-from-backup es la ruta DR. El rollback del consumidor es seguro por construcción (inbox + guarda de secuencia); el event-store de MMS es la ruta de rehidratación |
+| Rollback | `git revert` del pin en el fleet repo → Flux reconcilia el anterior | `helm rollback` = solo break-glass, luego realinear git. **Nunca rollback a través de una migración contract**; restore-from-backup es la ruta DR. El rollback del consumidor es seguro por construcción (inbox + guarda de versión); UMS republica el retrato en el siguiente cambio del tenant, y su agregado es la ruta de rehidratación |
| Blue-green / canary | **Todavía no** — sin señal que analizar con 1–3 réplicas y sin spans OTel de MassTransit; revisitar en AKS con dashboards SLO vivos | — |
## 11. Versionado de contrato y eventos
-- **Paquete:** `Evolith.Messaging.Contracts` (NuGet, publicado desde el repo MMS). **El namespace C# interno se mantiene `Evolith.Contracts.MasterData`** — MassTransit enruta por namespace+tipo; el namespace *es* el contrato de cable. Reemplaza las tres copias literales.
+- **Namespace:** `Evolith.Contracts.Tenancy` — un namespace de SUITE y no del emisor, porque MassTransit enruta por namespace+tipo; el namespace *es* el contrato de cable. **Hoy ningún paquete lo publica.** `Unimar.Ums.Sdk.Contracts` tiene metadatos de paquete pero nunca se publicó, así que `TenantSnapshotIntegrationEvent` está duplicado literalmente en UMS y en el Tracker. Dos copias de un tipo cuyo NOMBRE es la clave de enrutado divergen en silencio: renombrar un campo no rompe nada en compilación en ninguno de los dos repos y lo rompe todo en ejecución. Publicar ese paquete es el arreglo, y pertenece a esos repos.
- Cambio **aditivo** (campo opcional nuevo): bump minor; consumidores tolerantes; **desplegar consumidores primero, productor al final**.
-- Cambio **breaking**: nuevo major → **nuevo tipo de evento**; MMS **dual-publica** durante la ventana; **un consumidor se suscribe a EXACTAMENTE UN major de schema** (nunca ambos — dos message ids con la misma `sequence` hacen que la guarda descarte datos v2 de forma no determinista). Contract test del productor: v2.data ⊇ v1.data.
+- Cambio **breaking**: nuevo major → **nuevo tipo de evento**; UMS **dual-publica** durante la ventana; **un consumidor se suscribe a EXACTAMENTE UN major de schema** (nunca ambos — dos message ids con la misma `sequence` hacen que la guarda descarte datos v2 de forma no determinista). Contract test del productor: v2.data ⊇ v1.data.
- **Registry = git + CI:** fixtures JSON commiteados; el productor serializa y compara snapshot; los consumidores deserializan los mismos fixtures por su ruta real. Un servidor de registry (Apicurio, etc.) se rechaza hasta tener ≥3 familias de eventos.
- Los invariantes de orden/idempotencia (`sequence` monótono por tenant, `id` único, `subject`=tenantId) son parte del contrato; cambiarlos es breaking por definición.
-## 12. Migración de ownership del Tenant (el riesgo arquitectónico #1) — M0–M4
+## 12. Ownership del Tenant — cerrado el 2026-08-22
-Estado de dos-escritores hoy: UMS (`CreateTenantCommand` + `TenantEndpoints`) y Tracker (`CreateTenantCommandHandler`) siguen autorando tenants localmente contra la maestría de MMS. (El `DevTenantSeedHostedService` de Tracker **ya está gateado por ambiente dentro del servicio** — solo verificar, no es vector de producción.)
+Esta sección llevaba una escalera M0–M4 para mover la autoridad del tenant desde UMS y el Tracker **hacia MMS**. Está cerrada, y no porque se subiera la escalera: MMS nunca se construyó, así que la autoridad hacia la que migraba jamás existió.
-| Fase | Acción | Gate de salida |
-|---|---|---|
-| M0 — Plumb | Cableado broker + MasterDataDb en charts de ums/tracker; commit del DI gating de UMS; fix de fallbacks `Default`; aplicar fixes §5.5 | Matriz F1–F3 verde en kind |
-| M1 — Backfill | Exportar tenants locales existentes → `POST /tenants` en MMS (MMS se vuelve autoridad de IDs; mantener mapa local→master); el event-store los reproduce a las proyecciones | Reconciliación: proyecciones == MMS, cero drift |
-| M2 — Freeze writers | Feature-flag OFF de las rutas de escritura de tenant en UMS/Tracker; creación solo vía MMS | Cero INSERTs locales de tenant por 7 días |
-| M3 — Switch reads | Authz (UMS) y frontera de gobernanza (Tracker) leen de `masterdata.tenant_projection` | Reconciliación 24 h cero-drift |
-| M4 — Contract | Borrar rutas de escritura locales, luego los aggregates; los datos satélite del tenant se re-clavean al tenantId maestro | ADR-0083 / T-037 → Accepted |
+Lo que pasó de verdad, el 2026-08-22:
+
+| Entonces (este plan) | Ahora |
+|---|---|
+| Dos escritores — UMS y el Tracker autoran tenants localmente | **UMS es el escritor.** Siempre lo fue: el agregado, los cinco comandos que mutan y los endpoints viven ahí (ADR-UMS-107) |
+| M1 backfill: exportar tenants locales a `POST /tenants` en MMS | No hay nada que backfillear — el dato maestro nunca salió de UMS |
+| M2/M3: congelar los escritores locales, cambiar las lecturas a la proyección | **Hecho por T-059**: `code`, `name`, `status` y la existencia del tenant los escribe solo `TenantSnapshotConsumer`; `display_name`, `contact_email`, `tier`, `settings` y la localización siguen siendo del Tracker, porque UMS no los conoce |
+| M4: borrar rutas de escritura y agregados locales | El agregado del Tracker se **conserva** a propósito — sustituirlo por una proyección pelada perdería los cuatro campos de arriba |
-Regla interina: entre M0 y M2, la creación local de tenants es solo dev/demo por política.
+El invariante que sí conviene arrastrar: el Tracker no debe convertirse en un segundo maestro de Tenant. Lo que lo protege no es una fase de migración, sino el reparto de escrituras de arriba más la guarda de versión de §5.5-2.
## 13. Escalera de gates (G0–G4)
| Gate | Dónde | Bloquea | Checks |
|---|---|---|---|
-| G0 — CI | cada PR por repo | merge | build, unit, **contract tests**, Trivy, CodeQL. *(UMS/Tracker parcialmente EXISTE; MMS BUILD)* |
+| G0 — CI | cada PR por repo | merge | build, unit, **contract tests**, Trivy, CodeQL. *(UMS/Tracker parcialmente EXISTE)* |
| G1 — Integración | nightly, kind efímero (sustrato + umbrella) | staging | **matriz F1–F7 automatizada** + las aserciones que exigieron los críticos: endpoint consumidor *arrancado* (salud del bus, no solo pod Ready), fila InboxState al consumir, un path de NetworkPolicy permitido Y uno denegado |
| G2 — Soak staging | ≥24 h por RC | candidatura RC | Resiliencia R1–R6 (matar broker → outbox drena; matar consumidor → catch-up; veneno → `_error` → reproceso), presupuestos P1–P3, reconciliación 24 h cero-drift, dashboards+alertas vivos |
| G3 — RC Stamped | `evolith-cli gate evaluate -p qa` (gate-F4) | PR de prod | Test Summary, Acceptance, Security scan, Integration evidence, Pyramid — el stamp F4 es check requerido en el PR de prod del fleet repo |
-| G4 — Post-deploy | prod, tras reconciliar Flux | marca sano / dispara rollback | smoke: health en todos los pods; tenant sintético create → proyección visible en UMS+Tracker dentro del SLO de lag → deactivate; profundidad `_error` sin cambio; ventana de error-rate de 30 min |
+| G4 — Post-deploy | prod, tras reconciliar Flux | marca sano / dispara rollback | smoke: health en todos los pods; tenant sintético create en UMS → proyección visible en el Tracker dentro del SLO de lag → deactivate; profundidad `_error` sin cambio; ventana de error-rate de 30 min |
-Reglas duras: las migraciones contract nunca viajan con features · orden consumidor-primero para cambios aditivos · sin promoción a prod de features de proyección hasta agendar la migración de ownership (fases M).
+Reglas duras: las migraciones contract nunca viajan con features · orden consumidor-primero para cambios aditivos · el reparto de escrituras del tenant (§12) es un invariante, no un hito — una acción gobernada que escriba `code`/`name`/`status` en el Tracker es una regresión aunque todo lo demás esté verde.
## 14. Observabilidad
Stack (ns `observability`, BUILD — las configs ya existen bajo `product/operations/`, nada las despliega aún): kube-prometheus-stack + Loki (single-binary) + Tempo + OTel Collector; provisioning de Grafana, alertas Prometheus y config de Tempo reutilizados de `product/operations/{grafana,alerts,otel,tempo}`. Perfil VPS: réplica única, métricas 7d/trazas 3d; AKS: PVCs ZRS, 30d.
-Deltas de código por producto (BUILD, prerequisito de G2): MMS no tiene OTel; a UMS le falta `AddSource("MassTransit")` (spans del consumidor invisibles); el consumidor de Tracker es solo ILogger. Métricas estándar: `masterdata_projection_applied/discarded_total`, lag del consumidor, profundidad de `_error`, histograma de latencia e2e (según el diseño canónico §11).
+Deltas de código por producto (BUILD, prerequisito de G2): a UMS le falta `AddSource("MassTransit")` (spans de productor/consumidor invisibles); el consumidor de Tracker es solo ILogger. Métricas a AÑADIR — ninguna existe hoy, y el par `masterdata_projection_applied/discarded_total` que este plan nombraba tampoco tuvo nunca emisor: contadores de aplicados/descartados en el consumidor del retrato, lag del consumidor, profundidad de `_error`, histograma de latencia e2e. Hasta que existan, la profundidad de la cola y la columna `ums_projection_version` son las únicas señales honestas de frescura (ver el runbook de reproceso).
## 15. Registro consolidado de riesgos (deduplicado, verificado)
+Las filas 1, 4, 5 y 7 se cerraron el 2026-08-22 con ADR-UMS-107 + T-059, y las 6, 11 y 14 eran de MMS: se van con el producto. Se dejan tachadas y no borradas, porque un registro de riesgos que pierde filas en silencio no se puede auditar.
+
| # | Riesgo | Sev | Owner | Mitigación | Fase |
|---|---|---|---|---|---|
-| 1 | Ownership de tenant con dos escritores | Crítica | Winston | Escalera M0–M4 (§12) | fases M |
-| 2 | ~~Exchange consistent-hash divide el tráfico entre consumidores~~ **corregido por decisión §5.2** | Crítica -> resuelto | Arq | Topología fanout de MassTransit | retiro de CRDs BUILD |
-| 3 | Conflicto de declaración CRD/código (406 → consumidor muerto silencioso) | Crítica | Arq | retirar CRDs de colas (§5.2); G1 verifica endpoint *arrancado* | M0 |
-| 4 | Inbox de dedup no cableado en consumidores | Mayor | DevOps | `UseEntityFrameworkOutbox` en endpoints (§5.5-1) + assert InboxState en G1 | M0 |
-| 5 | Carrera de concurrencia en proyección (regresión permanente) | Mayor | DevOps | upsert condicional set-based (§5.5-2) | M0 |
-| 6 | Probes de MMS requieren Development → CrashLoop en prod | Mayor | Infra | endpoints `/health` reales antes del primer deploy a staging | pre-staging |
-| 7 | Fallback `Default`/`DefaultConnection` → consumidores apuntando a localhost | Mayor | Infra | fix del fallback + `MasterDataDb` explícito en charts | M0 |
-| 8 | Migraciones al arranque compiten con replicas>1 | Mayor | Infra | patrón migrate-Job en toda la suite | pre-staging |
-| 9 | Alertas de veneno mirando la cola equivocada (DLX vs `_error`) | Mayor | Infra | alertas de profundidad `_error` + runbook de shovel (§5.3) | pre-staging |
-| 10 | Broker = dependencia crítica compartida | Mayor | Infra | outbox (probado) + quorum con ≥3 nodos + degradación solo-frescura (§5.6) | permanente |
-| 11 | Credenciales en texto plano en values de mms-helm; default-user compartido | Menor | Infra | usuarios de broker por producto (CRDs) + ESO/AKV | pre-staging |
-| 12 | Deriva de contrato entre 3 copias de TenantEvent | Menor | DevOps | `Evolith.Messaging.Contracts` + contract tests en G0 | M0–M1 |
+| 1 | ~~Ownership de tenant con dos escritores (UMS/Tracker autoran)~~ | Crítico -> **cerrado** | Winston | T-059 repartió las escrituras: UMS posee `code`/`name`/`status` y la existencia; el Tracker conserva `tier`, `contact_email`, `settings`, localización (§12) | hecho 2026-08-22 |
+| 2 | ~~El exchange consistent-hash reparte tráfico entre consumidores~~ **resuelto por la decisión §5.2** | Crítico -> resuelto | Arq | topología fanout gobernada por MassTransit | hecho en doc; retiro de CRDs BUILD |
+| 3 | Conflicto de declaración CRD/código (406 → consumidor muerto en silencio) | Crítico | Arq | retirar CRDs de cola (§5.2); G1 asserta endpoint *arrancado* | pre-staging |
+| 4 | ~~Dedup del inbox sin cablear en los consumidores~~ | Mayor -> **cerrado** | DevOps | `UseEntityFrameworkOutbox` en el endpoint dentro de `TenantSnapshotConsumerDefinition` (§5.5-1); G1 mantiene el assert de InboxState | hecho 2026-08-22 |
+| 5 | ~~Carrera de concurrencia en la proyección (regresión permanente)~~ | Mayor -> **cerrado** | DevOps | upsert set-based con guarda de versión (§5.5-2) | hecho 2026-08-22 |
+| 6 | ~~Probes de MMS requieren Development → CrashLoop en prod~~ | — | — | el producto no existe | retirado 2026-08-23 |
+| 7 | ~~Fallback `Default`/`DefaultConnection` → consumidores en localhost~~ | Mayor -> **sin objeto** | Infra | T-059 eliminó por completo el contexto `MasterDataDb` aparte | hecho 2026-08-22 |
+| 8 | Migraciones al arranque compiten con replicas>1 | Mayor | Infra | patrón migrate-Job en toda la suite (§5.5-3) | pre-staging |
+| 9 | Alertas de mensaje veneno mirando la cola equivocada | Mayor | Infra | alerta de profundidad `_error` sobre `tracker.tenant-snapshot_error` + runbook de shovel (§5.3). Hasta el 2026-08-22 las alertas nombraban dos colas de la era MMS; `TenantProjectionQueueMissing` habría disparado crítico para siempre | hecho en doc; deploy BUILD |
+| 10 | El bróker es dependencia crítica compartida | Mayor | Infra | outbox (probado) + quorum donde haya ≥3 nodos + degradación solo de frescura (§5.6) | permanente |
+| 11 | ~~Credenciales en texto plano en values de mms-helm~~; default-user compartido | Menor | Infra | usuarios de broker por producto (CRDs) + ESO/AKV — la mitad de mms-helm se retira con el producto | pre-staging |
+| 12 | Drift de contrato entre las copias del contrato de tenant | Menor | DevOps | hoy dos copias literales (UMS, Tracker) porque `Unimar.Ums.Sdk.Contracts` nunca se publicó; publicarlo + contract tests en G0 (§11) | permanente |
| 13 | El CNI de kind no aplica NetworkPolicies (trampa de paridad) | Menor | Infra | Cilium en kind + aserciones allow/deny en G1 | pre-staging |
-| 14 | El CRUD de tenants de MMS no tiene authN | Menor | Arq | authN antes de cualquier exposición por ingress | pre-staging |
-| 15 | Comportamiento del consumidor no observable | Menor | Infra | deltas de código §14 como prerequisito de G2 | pre-staging |
+| 14 | ~~El CRUD de tenants de MMS no tiene authN~~ | — | — | el producto no existe | retirado 2026-08-23 |
+| 15 | Comportamiento del consumidor no observable (sin trazas/métricas de MassTransit) | Menor | Infra | deltas de código §14 como prerequisito de G2 — y ojo: las métricas de §14 no existen todavía, con ningún nombre | pre-staging |
## 16. Backlog BUILD priorizado
-**P0 (bloquea staging):** retirar CRDs de ruta de mensajes + Users/Permissions por producto · fixes de consumidor §5.5 (inbox, upsert condicional, migrate-Jobs, fallback `Default`) · endpoints de health en MMS · commit del DI gating de UMS · pipeline CI de MMS · `MasterDataDb`/`RabbitMq` explícitos en charts de ums/tracker.
-**P1 (bloquea prod):** paquete `Evolith.Messaging.Contracts` + contract tests · despliegue del stack de observabilidad + deltas de código · NetworkPolicies + Quotas + Cilium-en-kind · secretos a ESO/OpenBao · bootstrap de Flux + overlays `clusters/*` + tags ordenables · automatización G1 (matriz F1–F7 + las tres aserciones nuevas) · CNPG + backups off-node · reconstruir ums-helm sobre el template set de Tracker · re-alojar charts de Core al ns `evolith-core`.
+**P0 (bloquea staging):** retirar CRDs de ruta de mensajes + Users/Permissions por producto · el único punto de §5.5 que sigue abierto (migrate-Jobs con replicas>1) · commit del DI gating de UMS · `RabbitMq` explícito en los charts de ums/tracker.
+**P1 (bloquea prod):** publicar el paquete de contratos compartido para que `TenantSnapshotIntegrationEvent` deje de vivir en dos copias, + contract tests · despliegue del stack de observabilidad + deltas de código · NetworkPolicies + Quotas + Cilium-en-kind · secretos a ESO/OpenBao · bootstrap de Flux + overlays `clusters/*` + tags ordenables · automatización G1 (matriz F1–F7 + las tres aserciones nuevas) · CNPG + backups off-node · reconstruir ums-helm sobre el template set de Tracker · re-alojar charts de Core al ns `evolith-core`.
**P2 (hardening/crecimiento):** overlay AKS (Terraform/Bicep + AKV CSI + Flexible Servers) · umbrella chart E2E local · hardening de pods (non-root, rootfs RO) · dashboards SLO → revisitar canary · external-dns · revisión semanal de `_error`.
-## 17. How-to (16 pasos)
+## 17. How-to
| # | Paso | Comandos / assets |
|---|---|---|
@@ -317,14 +316,13 @@ Deltas de código por producto (BUILD, prerequisito de G2): MMS no tiene OTel; a
| 2 | Namespaces | `kubectl apply -f deploy/kubernetes/namespaces.yaml` |
| 3 | Broker | instalar Cluster-Operator; `kubectl apply -f deploy/kubernetes/messaging/rabbitmq-cluster.yaml` (+ overlay de réplicas); CRDs `User`/`Permission` por producto (BUILD) |
| 4 | Core | charts `evolith-core-api|mcp|agent-runtime` re-valuados (`product/infra/helm/`, `local-test.sh` construye/carga) → ns `evolith-core` |
-| 5 | MMS | `helm upgrade -i mms mms/infra/mms-helm -n mms -f values-local.yaml` (tras fix de health) |
-| 6 | UMS | ums-helm reconstruido (template set de Tracker) con `ConnectionStrings__RabbitMq` + `__MasterDataDb`; DI gating commiteado |
-| 7 | Tracker | charts `evolith_tracker/product/infra/helm/*` + las dos claves de conexión en secret.yaml |
+| 5 | UMS | ums-helm reconstruido (template set de Tracker) con `ConnectionStrings__RabbitMq`; DI gating commiteado |
+| 6 | Tracker | charts `evolith_tracker/product/infra/helm/*` + las claves de conexión en secret.yaml |
| 8 | Ingress/DNS/TLS | chart pineado de Traefik + cert-manager + IngressRoutes; `/etc/hosts` `*.evolith.local` |
| 9 | Bases de datos | Postgres por producto (StatefulSet local); correr migrate-Jobs |
| 10 | Secretos/config | Secrets planos en local; claves de conexión estandarizadas; bug de fallback corregido |
| 11 | Observabilidad | nuevo `deploy/kubernetes/observability/` desplegando kube-prometheus-stack/Loki/Tempo/OTel desde las configs de `product/operations/` |
-| 12 | Tests de integración | automatizar la matriz F1–F7 (filas de proyección en las BDs de UMS+Tracker) |
+| 12 | Tests de integración | automatizar la matriz F1–F7 (la fila de proyección en la BD del Tracker, con su `ums_projection_version`) |
| 13 | Validación RabbitMQ | R1–R6: matar broker → outbox drena; matar consumidor → catch-up; veneno → `_error` → shovel de vuelta; mgmt UI :30672 |
| 14 | Gates pre-staging | Musts de la matriz verdes + dashboards + alertas + 24 h cero-drift |
| 15 | **Promoción a staging** | 15a k3s en la VPS (o K8s gestionado por Coolify); 15b bootstrap de Flux (`flux bootstrap github …`), aplicar `clusters/staging/`; 15c ESO + OpenBao (`ClusterSecretStore` + `ExternalSecret` por ns); 15d issuer LE **staging** + DNS `*.stg`; 15e values-staging (réplicas 1, quotas §4.3); 15f correr el checklist de soak G2 |
@@ -332,4 +330,4 @@ Deltas de código por producto (BUILD, prerequisito de G2): MMS no tiene OTel; a
## 18. Referencias
-ADR-0106 · ADR-0107 · ADR-0033 · `mms/docs/architecture/tenant-master-data-projection.md` (flujo canónico) + `tenant-projection-test-matrix.md` · `deploy/kubernetes/README.md` (bring-up del sustrato) · `product/infra/deployment-topology.md` (mapa canónico de nombres) · `product/infra/helm/README.md` (charts de Core) · `product/infra/vps-coolify/README.md` · `product/operations/` (alertas, grafana, otel, tempo, load-tests, runbooks de incident-response) · UMS ADR-0083 · Tracker ADR T-037.
+ADR-0129 · ADR-0107 · ADR-0033 · `deploy/kubernetes/README.md` (bring-up del sustrato) · `product/infra/deployment-topology.md` (mapa canónico de nombres) · `product/infra/helm/README.md` (charts de Core) · `product/infra/vps-coolify/README.md` · `product/operations/` (alertas, grafana, otel, tempo, load-tests, runbooks de incident-response) · ADR-UMS-107 (UMS publica el tenant) · Tracker ADR T-059 (el Tracker lo proyecta) · Tracker ADR T-037.
diff --git a/product/suite/architecture/evolith-suite-deployment-strategy.md b/product/suite/architecture/evolith-suite-deployment-strategy.md
index 7cc9bca0..a0f029cc 100644
--- a/product/suite/architecture/evolith-suite-deployment-strategy.md
+++ b/product/suite/architecture/evolith-suite-deployment-strategy.md
@@ -3,9 +3,11 @@
# Evolith Suite — Deployment Strategy (Single-Cluster Kubernetes)
> **Status:** Proposed (BMAD consolidated) · **Owner:** Evolith Architecture Board
-> **Authority:** [ADR-0107](../../../reference/core/architecture/adrs/core/0107-single-cluster-kubernetes-deployment-topology.md) (single-cluster topology) · [ADR-0106](../../../reference/core/architecture/adrs/core/0106-master-tenant-context-projections.md) (tenant projections) · canonical flow design: `mms/docs/architecture/tenant-master-data-projection.md`
-> **Method:** produced by a BMAD multi-agent analysis — Winston (Architect), DevOps expert, Infrastructure expert — grounded in the real state of the four repos (evolith, mms, ums, evolith_tracker), then adversarially verified by grounding/completeness/operational critics. Verified corrections are folded in (see §5, §15).
-> **Date:** 2026-07-09
+> **Authority:** [ADR-0107](../../../reference/core/architecture/adrs/core/0107-single-cluster-kubernetes-deployment-topology.md) (single-cluster topology) · [ADR-0129](../../../reference/core/architecture/adrs/core/0129-ums-is-the-tenant-master.md) (the tenant master is UMS) · satellite decisions: ADR-UMS-107 (emitter) and T-059 (consumer)
+> **Method:** produced by a BMAD multi-agent analysis — Winston (Architect), DevOps expert, Infrastructure expert — grounded in the real state of the repos, then adversarially verified by grounding/completeness/operational critics. Verified corrections are folded in (see §5, §15).
+> **Date:** 2026-07-09 · **Rebased on the three-product suite:** 2026-08-23
+
+> **What changed on 2026-08-23.** This plan was written for four products, one of which — **MMS** — never existed. ADR-0106 named it the tenant master; ADR-0129 supersedes that, and the two satellites had already moved: UMS owns and publishes the tenant (ADR-UMS-107), the Tracker consumes it (T-059). Everything MMS-shaped is gone from this document: its namespace, its database, its CI pipeline, its risks, and the M0–M4 ownership migration that existed to hand it authority it was never going to hold. The messaging §5 is redrawn around **one** producer and **one** consumer queue.
---
@@ -24,8 +26,8 @@
| Secrets | K8s Secrets (local) → **OpenBao + ESO** (VPS) → **Azure Key Vault + CSI** (AKS); same Secret names so charts never change | §7 |
| Ingress | **Traefik everywhere** (Core+Tracker charts already use IngressRoute; k3s bundles it; UMS chart rebuilt onto the Tracker template set) · 1 public IP + host routing · cert-manager + Let's Encrypt | §7 |
| Deployment strategy | **RollingUpdate everywhere** (`maxSurge:1, maxUnavailable:0` + PDB); no blue-green/canary until real traffic + SLO dashboards exist | §10 |
-| Contract | Shared **`Evolith.Messaging.Contracts`** NuGet (package id), C# namespace stays **`Evolith.Contracts.MasterData`** (MassTransit routes by namespace+type); expand-contract; **one schema major per consumer** | §11 |
-| Ownership migration | Five gated phases **M0–M4** (plumb → backfill → freeze writers → switch reads → contract) | §12 |
+| Contract | Namespace **`Evolith.Contracts.Tenancy`** — a SUITE namespace, not the emitter's, because MassTransit routes by namespace+type; expand-contract; **one schema major per consumer**. No package publishes it today: the type is duplicated verbatim in UMS and the Tracker | §11 |
+| Ownership migration | **Closed 2026-08-22** by ADR-UMS-107 + T-059 — UMS was always the writer; the Tracker stopped authoring what UMS owns | §12 |
| Promotion gates | **G0–G4 ladder**; G3 = the existing Evolith gate machinery (`evolith-cli gate evaluate -p qa`, gate-F4 "RC Stamped") | §13 |
| Probes | **Readiness NEVER gates on AMQP** — broker outage degrades freshness, it must not drain the HTTP fleet | §5.4 |
@@ -52,24 +54,21 @@
```mermaid
flowchart LR
subgraph MASTER["Authority — master data"]
- MMS["MMS
Tenant MASTER writer of record
ADR-0106"]
+ UMS["UMS
Tenant MASTER writer of record
+ identity & authz
ADR-0129 · ADR-UMS-107"]
end
subgraph PLATFORM["Shared platform — ADR-0107"]
- BUS["RabbitMQ
TenantEvent (CloudEvents envelope)"]
+ BUS["RabbitMQ
TenantSnapshotIntegrationEvent (CloudEvents envelope)"]
OBS["Observability
OTel · Prometheus · Tempo · Grafana"]
end
subgraph CONSUMERS["Consumers — read-only projections"]
- UMS["UMS
identity & authz
masterdata.tenant_projection"]
- TRK["Evolith Tracker
SDLC governance
masterdata.tenant_projection"]
+ TRK["Evolith Tracker
SDLC governance
tenants + ums_projection_version"]
end
CORE["Evolith Core
stateless evaluation engine
HTTP only — NEVER on the broker"]
- MMS -- "transactional outbox (ADR-0033)" --> BUS
- BUS -- "queue ums.tenant-projection
inbox + sequence guard" --> UMS
- BUS -- "queue tracker.tenant-projection
inbox + sequence guard" --> TRK
+ UMS -- "transactional outbox (ADR-0033)" --> BUS
+ BUS -- "queue tracker.tenant-snapshot
inbox + version guard" --> TRK
UMS -- "HTTP evaluate" --> CORE
TRK -- "HTTP evaluate" --> CORE
- MMS -.-> OBS
UMS -.-> OBS
TRK -.-> OBS
CORE -.-> OBS
@@ -83,9 +82,8 @@ flowchart TB
LB["Traefik ingress — LoadBalancer 80/443 + TLS"]
subgraph NSMSG["ns evolith-messaging"]
RMQ["RabbitmqCluster evolith-rabbitmq
replicas: 1 local+VPS · 3 AKS"]
- EX["MassTransit type exchange (fanout)
Evolith.Contracts.MasterData:TenantEvent"]
- QU["queue ums.tenant-projection
+ ums.tenant-projection_error"]
- QT["queue tracker.tenant-projection
+ tracker.tenant-projection_error"]
+ EX["MassTransit type exchange (fanout)
Evolith.Contracts.Tenancy:TenantSnapshotIntegrationEvent"]
+ QT["queue tracker.tenant-snapshot
+ tracker.tenant-snapshot_error"]
end
subgraph NSOBS["ns observability (BUILD)"]
OTEL["otel-collector"] --- PROM["Prometheus"] --- TEMPO["Tempo"] --- GRAF["Grafana"]
@@ -93,23 +91,19 @@ flowchart TB
subgraph NSCORE["ns evolith-core"]
COREAPI["core-api"] --- MCP["evolith-mcp"] --- AGRT["agent-runtime"]
end
- subgraph NSMMS["ns mms"]
- MMSAPI["mms-api"] --- MMSDB[("Postgres mms
masterdata: tenants, event store, outbox")]
- end
subgraph NSUMS["ns ums"]
- UMSAPI["ums-api"] --- UMSWEB["ums-web"] --- UMSDB[("Postgres ums
+ masterdata.tenant_projection + inbox")]
+ UMSAPI["ums-api"] --- UMSWEB["ums-web"] --- UMSDB[("Postgres ums
tenants (master) + outbox")]
end
subgraph NSTRK["ns tracker"]
- TRKAPI["tracker-api"] --- TRKWEB["tracker-web"] --- TRKDB[("Postgres tracker
+ masterdata.tenant_projection + inbox")]
+ TRKAPI["tracker-api"] --- TRKWEB["tracker-web"] --- TRKDB[("Postgres tracker
tenants + ums_projection_version + inbox")]
end
end
LB --> UMSWEB & TRKWEB & COREAPI
- MMSAPI -->|"AMQP · user mms"| EX
- EX --> QU & QT
- QU -->|"consume · user ums"| UMSAPI
+ UMSAPI -->|"AMQP · user ums"| EX
+ EX --> QT
QT -->|"consume · user tracker"| TRKAPI
UMSAPI & TRKAPI -->|"HTTP core-api.evolith-core.svc"| COREAPI
- MMSAPI & UMSAPI & TRKAPI -.->|OTLP| OTEL
+ UMSAPI & TRKAPI -.->|OTLP| OTEL
RMQ -.->|":15692"| PROM
```
@@ -126,7 +120,7 @@ Multi-cluster-per-product is **rejected**: it forces RabbitMQ federation/shovel
| Nodes | 1 (`deploy/kubernetes/kind-cluster.yaml`) | 1 | 1–2 (RAM-bound: 7.8 GB class) | system 2×B2s + user 3×D4as_v5 across 3 AZs, autoscaler 3→6 |
| CNI | **Cilium** (install with `disableDefaultCNI: true` — kindnet does not enforce NetworkPolicy; parity with AKS) | k3s default or Cilium | idem | Azure CNI Overlay + **Cilium** dataplane |
| RabbitMQ | replicas **1** (values overlay) | replicas 1, 5Gi PV | replicas **1** (3 replicas on one node is fake HA and triples RAM; quorum=3 only at ≥3 nodes) | replicas 3, zone anti-affinity, ZRS PVs |
-| Postgres | in-cluster StatefulSet per product | **CloudNativePG** per product + off-node WAL/base backups (S3-compatible) | idem | **Azure Database for PostgreSQL Flexible Server** per product (zone-redundant for MMS — master authority; burstable for projections) |
+| Postgres | in-cluster StatefulSet per product | **CloudNativePG** per product + off-node WAL/base backups (S3-compatible) | idem | **Azure Database for PostgreSQL Flexible Server** per product (zone-redundant for UMS — master authority; burstable for the Tracker's projection) |
| Storage class | kind default | local-path | local-path + mandatory off-node backups | managed-csi / premium+ZRS for broker & DB |
| TLS | none/mkcert | cert-manager + LE staging issuer | cert-manager + LE prod | cert-manager + LE prod |
| Secrets | plain K8s Secrets | OpenBao + ESO | OpenBao + ESO (GT-112) | Azure Key Vault + CSI + workload identity |
@@ -135,22 +129,21 @@ Multi-cluster-per-product is **rejected**: it forces RabbitMQ federation/shovel
### 4.2 HA posture per environment (explicit)
-- **prod-VPS: no HA by design.** Availability = fast restore: CNPG PITR + off-node WAL, broker quorum-of-1 on durable PVs, documented RTO (≤30 min) / RPO (≤5 min via WAL). The MMS **outbox** makes broker downtime lossless for producers; consumers catch up. **Trigger to real HA:** ≥3 nodes → broker replicas 3 + CNPG replicas.
-- **prod-AKS: the real HA tier.** 3-AZ node spread, broker quorum 3 with zone anti-affinity, zone-redundant MMS Postgres.
+- **prod-VPS: no HA by design.** Availability = fast restore: CNPG PITR + off-node WAL, broker quorum-of-1 on durable PVs, documented RTO (≤30 min) / RPO (≤5 min via WAL). The UMS **outbox** makes broker downtime lossless for the producer; the consumer catches up. **Trigger to real HA:** ≥3 nodes → broker replicas 3 + CNPG replicas.
+- **prod-AKS: the real HA tier.** 3-AZ node spread, broker quorum 3 with zone anti-affinity, zone-redundant UMS Postgres.
### 4.3 Sizing (requests/limits — derive namespace ResourceQuotas from this)
| Component | requests (VPS) | limits (VPS) | AKS |
|---|---|---|---|
-| mms-api | 100m / 128Mi | 500m / 384Mi | 250m/256Mi → 1/512Mi |
| ums-api | 150m / 256Mi | 750m / 768Mi | 500m/512Mi → 1/1Gi |
| ums-web / tracker-web (nginx) | 25m / 32Mi | 100m / 64Mi | idem |
| tracker-api | 150m / 256Mi | 750m / 768Mi | 500m/512Mi → 1/1Gi |
| core-api + mcp + agent-runtime (each) | 100m / 192Mi | 500m / 512Mi | 250m/256Mi → 1/768Mi |
-| Postgres ×3 (in-cluster) | 100m / 256Mi each | 500m / 512Mi each | managed (n/a) |
+| Postgres ×2 (in-cluster) | 100m / 256Mi each | 500m / 512Mi each | managed (n/a) |
| RabbitMQ (replicas 1) | 250m / 512Mi | 1 / 1Gi | ×3 @ 500m/1Gi → 1/2Gi |
| Observability (min profile) | 300m / 1Gi total | 1 / 2Gi total | full profile 2 / 4Gi |
-| **Total (VPS, requests)** | **≈1.6 vCPU / ≈3.6 GiB** | fits 2 vCPU / 7.8 GB with headroom | — |
+| **Total (VPS, requests)** | **≈1.4 vCPU / ≈3.1 GiB** | fits 2 vCPU / 7.8 GB with headroom | — |
---
@@ -159,31 +152,33 @@ Multi-cluster-per-product is **rejected**: it forces RabbitMQ federation/shovel
The adversarial verification found that the previously-declared CRD topology **does not match how MassTransit actually moves messages**. Three verified defects and their resolutions:
### 5.1 `x-consistent-hash` cannot fan out (critical, fixed by design change)
-A consistent-hash exchange routes each message to **exactly one** bound queue — with `ums.tenant-projection` and `tracker.tenant-projection` both bound, each event would reach **either** UMS **or** Tracker (~50/50 by tenantId hash), never both. **Rule:** consistent-hash is a *partitioning tool inside one consumer group*, never a pub/sub distribution tool. Fan-out needs a fanout/topic exchange with one binding per consumer group.
+A consistent-hash exchange routes each message to **exactly one** bound queue. With two consumer queues bound — the shape this plan assumed — each event would have reached **either** one **or** the other (~50/50 by tenantId hash), never both. The suite has one consumer today (`tracker.tenant-snapshot`), so the split cannot bite yet, and that is exactly why the rule is written down rather than deleted: it would bite silently the day a second consumer group binds. **Rule:** consistent-hash is a *partitioning tool inside one consumer group*, never a pub/sub distribution tool. Fan-out needs a fanout/topic exchange with one binding per consumer group.
### 5.2 MassTransit owns the message topology (critical, decision)
-MassTransit auto-declares a **fanout type-exchange** (`Evolith.Contracts.MasterData:TenantEvent`) and binds each consumer endpoint's exchange/queue to it — that is the topology the validated E2E actually flowed through; the CRD exchange was dead weight, and CRD-pre-created queues with DLX arguments would make MassTransit's re-declare fail (`406 PRECONDITION_FAILED` → endpoint faults forever while the pod stays Ready — the classic silent 3 a.m. failure).
+MassTransit auto-declares a **fanout type-exchange** (`Evolith.Contracts.Tenancy:TenantSnapshotIntegrationEvent`) and binds each consumer endpoint's exchange/queue to it — that is the topology the validated E2E actually flowed through; the CRD exchange was dead weight, and CRD-pre-created queues with DLX arguments would make MassTransit's re-declare fail (`406 PRECONDITION_FAILED` → endpoint faults forever while the pod stays Ready — the classic silent 3 a.m. failure).
**Decision — embrace MassTransit conventions:**
- **Retire** the `Exchange`/`Queue`/`Binding` CRDs in `deploy/kubernetes/messaging/tenant-topology.yaml` for the message path.
- **Keep** Topology-Operator CRDs for what MassTransit cannot declare: per-product **`User`/`Permission`** (and optional `Policy`) CRDs.
-- Consumer endpoint names stay pinned in code (`ums.tenant-projection`, `tracker.tenant-projection` consumer definitions).
-- Broker permissions as **regex over naming prefixes** (verb-only grants break MassTransit startup): `mms` → configure/write on `^(Evolith\.Contracts\.MasterData.*|mms\..*)$`; `ums` → configure/write/read on `^(ums\..*|Evolith\.Contracts\.MasterData.*)$`; `tracker` symmetric.
+- The consumer endpoint name stays pinned in code (`tracker.tenant-snapshot`, in `TenantSnapshotConsumerDefinition`).
+- Broker permissions as **regex over naming prefixes** (verb-only grants break MassTransit startup): `ums` → configure/write on `^(Evolith\.Contracts\.Tenancy.*|ums\..*)$`; `tracker` → configure/write/read on `^(tracker\..*|Evolith\.Contracts\.Tenancy.*)$`.
### 5.3 Poison messages land in `_error`, not a DLX (major, decision)
-After retries are exhausted MassTransit **moves** the faulted message to `_error` — it never nacks, so broker `x-dead-letter-exchange` never fires. **Decision:** adopt the MassTransit convention — alert on `ums.tenant-projection_error` / `tracker.tenant-projection_error` depth > 0; the reprocess runbook shovels from `_error` back to the main queue; the DLX/DLQ CRDs are retired with §5.2.
+After retries are exhausted MassTransit **moves** the faulted message to `_error` — it never nacks, so broker `x-dead-letter-exchange` never fires. **Decision:** adopt the MassTransit convention — alert on `tracker.tenant-snapshot_error` depth > 0; the reprocess runbook shovels from `_error` back to the main queue; the DLX/DLQ CRDs are retired with §5.2.
### 5.4 Probe rule (resolved contradiction)
**`/health/ready` checks the product's own DB only. Broker connectivity NEVER gates readiness** — the projection consumers live inside `ums-api`/`tracker-api`; gating readiness on AMQP would turn any broker outage into a full suite HTTP outage (auth included). Broker health is a separate degraded-mode signal: metric + alert (`bus disconnected`, `projection lag`).
-### 5.5 P0 consumer-correctness code fixes (verified defects in current code)
-1. **Inbox not actually wired:** both repos call `AddEntityFrameworkOutbox()` at bus level but the consumer definitions never call `endpointConfigurator.UseEntityFrameworkOutbox(context)` — `InboxState` exists but is never consulted. Add it in both `TenantProjectionConsumerDefinition`s.
-2. **Read-check-write race:** the versioned upsert has no concurrency token; two in-flight events for one tenant can permanently regress the projection. Fix with a set-based conditional write: `INSERT … ON CONFLICT (tenant_id) DO UPDATE SET … WHERE tenant_projection.version < EXCLUDED.version` (cheapest; also removes a round-trip).
-3. **Startup migrations race at replicas>1** (MMS `Program.cs` MigrateAsync, UMS/Tracker migrators): adopt Tracker's **migrate-Job** Helm-hook pattern (`evolith_tracker/product/infra/helm/evolith-tracker-api/templates/migrate-job.yaml`) suite-wide.
-4. **`Default` vs `DefaultConnection` fallback bug** (UMS + Tracker DI): the projection context silently targets hardcoded localhost when `MasterDataDb` is unset — always set `ConnectionStrings__MasterDataDb` explicitly in charts and fix the fallback.
+### 5.5 Consumer correctness — three of four defects are closed
+This section listed four P0 defects found in the MMS-era consumers. T-059 rebuilt that consumer against the UMS snapshot and closed three of them; they are kept here because the *reasons* are what a future consumer has to satisfy, not because the work is open.
+
+1. ~~**Inbox not actually wired**~~ — **closed**. `TenantSnapshotConsumerDefinition` now calls `endpointConfigurator.UseEntityFrameworkOutbox(context)` on the endpoint, which is what makes `InboxState` actually consulted; the bus-level `AddEntityFrameworkOutbox` never did. Same context the consumer writes to, so dedup and projection land in one transaction.
+2. ~~**Read-check-write race**~~ — **closed**. The upsert is set-based and guarded: `ON CONFLICT (id) DO UPDATE … WHERE ums_projection_version < EXCLUDED.ums_projection_version`. The guard covers what the inbox cannot — redelivery after a restart and out-of-order delivery, which for a broker are normal behaviour.
+3. **Startup migrations race at replicas>1** — **still open** for UMS and the Tracker: adopt Tracker's **migrate-Job** Helm-hook pattern (`evolith_tracker/product/infra/helm/evolith-tracker-api/templates/migrate-job.yaml`) suite-wide.
+4. ~~**`Default` vs `DefaultConnection` fallback bug**~~ — **moot**. T-059 removed the separate `MasterDataDb` connection string; the projection lands in the Tracker's own schema, so there is no second context left to fall back to localhost.
### 5.6 Dependency semantics
-Producer: the MMS transactional outbox (validated live) makes broker outages **lossless** — writes commit, events drain on reconnect. Consumers: idle and catch up. Broker outage degrades **freshness only, never correctness**. No init-container ordering, no startup waits.
+Producer: the UMS transactional outbox (validated live) makes broker outages **lossless** — writes commit, events drain on reconnect. The consumer idles and catches up. Broker outage degrades **freshness only, never correctness**. No init-container ordering, no startup waits.
---
@@ -191,22 +186,22 @@ Producer: the MMS transactional outbox (validated live) makes broker outages **l
| Axis | Decision |
|---|---|
-| DB | DB-per-product; `masterdata` schema per repo (MMS master; UMS/Tracker projection) — no cross-product DB access, enforced by NetworkPolicy + distinct credentials |
-| Config | One ConfigMap per product namespace, rendered by its own chart; standardized keys: `DefaultConnection`, `MasterDataDb`, `RabbitMq` |
+| DB | DB-per-product (UMS holds the tenant master; the Tracker holds its projection in its own schema) — no cross-product DB access, enforced by NetworkPolicy + distinct credentials |
+| Config | One ConfigMap per product namespace, rendered by its own chart; standardized keys: `DefaultConnection`, `RabbitMq` |
| Secrets | `-db`, `-broker` per namespace; **per-product broker users** via CRDs (shared `default-user` rejected: one leak = suite-wide blast radius) |
| Compute | ResourceQuota + LimitRange per ns (values in §4.3); HPA per deployment; PDB `minAvailable:1` where replicas≥2 |
| Monitoring | ServiceMonitor + PrometheusRule per product, shipped **inside its chart**, discovered by shared Prometheus via `evolith.dev/product` label |
| Logs | stdout JSON → Alloy/Promtail → Loki (namespace label) |
-| Traces | OTel SDK → shared collector → Tempo; `correlationId` from the envelope is the join key; UMS must add `AddSource("MassTransit")`; MMS must propagate incoming `traceparent` |
+| Traces | OTel SDK → shared collector → Tempo; `correlationId` from the envelope is the join key; UMS must add `AddSource("MassTransit")` |
| Health | `/health/live` (process) + `/health/ready` (own DB only — §5.4) |
| Releases | One Helm release per product; umbrella chart local-only (ADR-0107 §6) |
## 7. Ingress, networking, DNS, TLS, NetworkPolicies
- **Traefik everywhere** (Core + Tracker charts already template `IngressRoute`; the VPS already runs Traefik under Coolify; k3s bundles it — disable bundled, install the pinned chart). UMS's disabled-by-default Gateway-API `httproute.yaml` is retired when the UMS chart is rebuilt on the Tracker template set. Traefik v3 also implements Gateway API — no door closed.
-- **Exposure: 1 public IP + host-based routing** (per-service IPs rejected — cost + DNS sprawl, no isolation gain). Hosts: `mms|ums|tracker|core.`; local `*.evolith.local` in /etc/hosts; staging `*.stg.`; keep `product/infra/deployment-topology.md` as the canonical name map.
-- Internal east-west: ClusterIP + cluster DNS only — broker `evolith-rabbitmq.evolith-messaging.svc:5672`, Core `core-api.evolith-core.svc`. Only user-facing frontends/APIs get IngressRoutes. ⚠️ **MMS tenant CRUD has no authentication today — authN is a hard precondition to any ingress exposure of MMS.**
-- **NetworkPolicy: default-deny ingress+egress per product namespace**, explicit allows: `{mms,ums,tracker}→evolith-messaging:5672` · `{ums,tracker}→evolith-core:HTTP` · `ingress→products:8080` · `observability→all:metrics` · `each product→own DB:5432` · `all→kube-dns:53` (+ OTLP 4317, cert-manager solver). **Structural rule: `evolith-core` gets NO path to 5672** — "Core never on the broker" enforced by the network. Local kind must run **Cilium** or the whole model is silently unenforced (§4.1).
+- **Exposure: 1 public IP + host-based routing** (per-service IPs rejected — cost + DNS sprawl, no isolation gain). Hosts: `ums|tracker|core.`; local `*.evolith.local` in /etc/hosts; staging `*.stg.`; keep `product/infra/deployment-topology.md` as the canonical name map.
+- Internal east-west: ClusterIP + cluster DNS only — broker `evolith-rabbitmq.evolith-messaging.svc:5672`, Core `core-api.evolith-core.svc`. Only user-facing frontends/APIs get IngressRoutes.
+- **NetworkPolicy: default-deny ingress+egress per product namespace**, explicit allows: `{ums,tracker}→evolith-messaging:5672` · `{ums,tracker}→evolith-core:HTTP` · `ingress→products:8080` · `observability→all:metrics` · `each product→own DB:5432` · `all→kube-dns:53` (+ OTLP 4317, cert-manager solver). **Structural rule: `evolith-core` gets NO path to 5672** — "Core never on the broker" enforced by the network. Local kind must run **Cilium** or the whole model is silently unenforced (§4.1).
## 8. Persistence & backups
@@ -214,7 +209,7 @@ Producer: the MMS transactional outbox (validated live) makes broker outages **l
|---|---|---|
| local | StatefulSet per product (fix UMS chart's `emptyDir` Postgres → PVC) | 1 replica, PV |
| staging / prod-VPS | **CloudNativePG** per product; scheduled base backups + WAL archiving **off-node** (MinIO/Backblaze). An unmanaged StatefulSet with no backup story is not production | 1 replica, durable PV |
-| prod-AKS | **Flexible Server** per product (zone-redundant for MMS) | 3 replicas, premium ZRS |
+| prod-AKS | **Flexible Server** per product (zone-redundant for UMS) | 3 replicas, premium ZRS |
## 9. CI/CD & GitOps (Flux CD v2)
@@ -222,7 +217,7 @@ Producer: the MMS transactional outbox (validated live) makes broker outages **l
- **Image tags:** immutable `sha-<7>` on every merge to develop **plus an orderable tag `develop--`** — Flux ImagePolicy cannot order bare sha tags; staging automation keys on the timestamp pattern (`^develop-[a-f0-9]+-(?P[0-9]+)`, numerical asc). Release tags `X.Y.Z`. Registry: `ghcr.io/beyondnetcode/*`.
- **Charts:** SemVer per chart, published as OCI to `ghcr.io/beyondnetcode/charts/`.
- **Staging:** auto-bump by Flux Image Automation (git commit back = audit trail). **Prod:** exact chart + exact image pinned via PR to the fleet repo; the PR *is* the promotion event; the gate-F4 stamp is a required status check.
-- **Pipelines per repo (corrected baseline):** UMS **has** CI (build/test, SonarCloud, security, release-candidate, contract-validation workflows) and Tracker **has** CI (build+test with real Postgres, contract-conformance); **MMS has none**. BUILD: MMS full pipeline; image build+push + chart-publish + Trivy jobs in all four repos; Core's `docker-images.yml` extended with develop-sha builds.
+- **Pipelines per repo (corrected baseline):** UMS **has** CI (build/test, SonarCloud, security, release-candidate, contract-validation workflows) and Tracker **has** CI (build+test with real Postgres, contract-conformance). BUILD: image build+push + chart-publish + Trivy jobs in all three repos; Core's `docker-images.yml` extended with develop-sha builds.
```
PR ──G0──▶ develop ──▶ GHCR (sha + develop-sha-ts) ──▶ Flux bumps staging (auto)
@@ -234,82 +229,85 @@ PR ──G0──▶ develop ──▶ GHCR (sha + develop-sha-ts) ──▶ Flu
| Component | Strategy | Notes |
|---|---|---|
-| Stateless APIs | RollingUpdate `maxSurge:1,maxUnavailable:0` + PDB | Precondition: real `/health` endpoints in MMS (today probes hit `/openapi/v1.json`, Development-only → prod CrashLoop) |
+| Stateless APIs | RollingUpdate `maxSurge:1,maxUnavailable:0` + PDB | Every surface answers `/health/live` + `/health/ready` (§5.4) |
| Web SPAs | RollingUpdate | tracker-web's envsubst upstream pattern is the reference |
-| Projection consumers | Deploy freely — queue buffers; **ConcurrentMessageLimit/order handled by the §5.5 conditional upsert** | Scale-out later via per-group hash partitions, never by assuming order across competing consumers |
+| Projection consumer | Deploy freely — the queue buffers; **order is handled by the version-guarded upsert (§5.5-2)** | Scale-out later via per-group hash partitions, never by assuming order across competing consumers |
| Postgres / RabbitMQ | Operator-managed; never in product pipelines | Topology changes additive-only |
| Event schema | Expand-contract on the wire (§11) | Consumers first for additive; dual-publish for breaking |
| EF migrations | **Migrate-Job Helm hook** (Tracker pattern) — never at startup | §5.5 |
-| Rollback | `git revert` fleet-repo pin → Flux reconciles previous | `helm rollback` = break-glass only, then re-align git. **Never roll back across a contract migration**; restore-from-backup is the DR path. Consumer rollback is safe by construction (inbox + sequence guard); MMS event-store is the re-hydration path |
+| Rollback | `git revert` fleet-repo pin → Flux reconciles previous | `helm rollback` = break-glass only, then re-align git. **Never roll back across a contract migration**; restore-from-backup is the DR path. Consumer rollback is safe by construction (inbox + version guard); UMS republishes the snapshot on the next tenant change, and its aggregate is the re-hydration path |
| Blue-green / canary | **Not yet** — no signal to analyze at 1–3 replicas without MassTransit OTel spans; revisit on AKS with live SLO dashboards | — |
## 11. Contract & event versioning
-- **Package:** `Evolith.Messaging.Contracts` (NuGet, published from the MMS repo). **The C# namespace inside stays `Evolith.Contracts.MasterData`** — MassTransit routes by namespace+type; the namespace *is* the wire contract. Replaces the three verbatim copies.
+- **Namespace:** `Evolith.Contracts.Tenancy` — a SUITE namespace rather than the emitter's, because MassTransit routes by namespace+type; the namespace *is* the wire contract. **No package publishes it today.** `Unimar.Ums.Sdk.Contracts` carries package metadata but has never been published, so `TenantSnapshotIntegrationEvent` is duplicated verbatim in UMS and in the Tracker. Two copies of a type whose NAME is the routing key diverge silently: renaming a field breaks nothing at compile time in either repository and breaks everything at runtime. Publishing that package is the fix, and it belongs to those repos.
- **Additive** change (new optional field): minor bump; consumers are tolerant readers; **deploy consumers first, producer last**.
-- **Breaking** change: new major → **new event type**; MMS **dual-publishes** during the window; **a consumer version subscribes to EXACTLY ONE schema major** (never both — two message ids with the same `sequence` make the guard nondeterministically drop v2 data). Producer-side contract test: v2.data ⊇ v1.data.
+- **Breaking** change: new major → **new event type**; UMS **dual-publishes** during the window; **a consumer version subscribes to EXACTLY ONE schema major** (never both — two message ids with the same `sequence` make the guard nondeterministically drop v2 data). Producer-side contract test: v2.data ⊇ v1.data.
- **Registry = git + CI:** committed JSON fixtures; producer serializes and snapshot-compares; consumers deserialize the same fixtures through their real path. A registry server (Apicurio, etc.) is rejected until ≥3 event families.
-- Ordering/idempotency invariants (`sequence` monotonic per tenant, `id` unique, `subject`=tenantId) are part of the contract; changing them is breaking by definition.
+- Ordering/idempotency invariants (`Version` monotonic per tenant — a database sequence, because an aggregate `RowVersion` does not order and a timestamp ties — `id` unique, `subject`=tenantId) are part of the contract; changing them is breaking by definition.
-## 12. Tenant ownership migration (the #1 architectural risk) — M0–M4
+## 12. Tenant ownership — closed 2026-08-22
-Two-writer state today: UMS (`CreateTenantCommand` + `TenantEndpoints`) and Tracker (`CreateTenantCommandHandler`) still author tenants locally against MMS mastership. (Tracker's `DevTenantSeedHostedService` is **already environment-gated inside the service** — verify only, not a production vector.)
+This section carried an M0–M4 ladder to move tenant authority from UMS and the Tracker **to MMS**. It is closed, and not because the ladder was climbed: MMS was never built, so the authority it was migrating toward never existed.
-| Phase | Action | Exit gate |
-|---|---|---|
-| M0 — Plumb | Broker + MasterDataDb wiring in ums/tracker charts; commit UMS DI gating; fix `Default` fallbacks; apply §5.5 fixes | Matrix F1–F3 green on kind |
-| M1 — Backfill | Export existing local tenants → `POST /tenants` on MMS (MMS becomes ID authority; keep local→master ID map); event-store replays into projections | Reconciliation: projections == MMS, zero drift |
-| M2 — Freeze writers | Feature-flag OFF the UMS/Tracker tenant write paths; creation only via MMS | No local tenant INSERTs for 7 days |
-| M3 — Switch reads | Authz (UMS) and governance boundary (Tracker) read from `masterdata.tenant_projection` | 24 h zero-drift reconciliation |
-| M4 — Contract | Delete local write paths, then local aggregates; tenant-scoped satellite data re-keyed to master tenantId | ADR-0083 / T-037 → Accepted |
+What actually happened, on 2026-08-22:
-Interim rule: between M0 and M2, local tenant creation is dev/demo-only by policy.
+| Then (this plan) | Now |
+|---|---|
+| Two writers — UMS and the Tracker both author tenants locally | **UMS is the writer.** It always was: the aggregate, the five mutating commands and the endpoints live there (ADR-UMS-107) |
+| M1 backfill: export local tenants to `POST /tenants` on MMS | Nothing to backfill — the master data never left UMS |
+| M2/M3: freeze the local writers, switch reads to the projection | **Done by T-059**: `code`, `name`, `status` and the tenant's existence are written only by `TenantSnapshotConsumer`; `display_name`, `contact_email`, `tier`, `settings` and localisation stay the Tracker's, because UMS does not know them |
+| M4: delete local write paths and aggregates | The Tracker's aggregate is deliberately **kept** — replacing it with a bare projection would lose the four fields above |
+
+The one invariant worth carrying forward: the Tracker must not become a second tenant master. What guards it is not a migration phase but the write split above, plus the version guard in §5.5-2.
## 13. Gate ladder (G0–G4)
| Gate | Where | Blocks | Checks |
|---|---|---|---|
-| G0 — CI | every PR per repo | merge | build, unit, **contract tests**, Trivy, CodeQL. *(UMS/Tracker partially EXISTS; MMS BUILD)* |
+| G0 — CI | every PR per repo | merge | build, unit, **contract tests**, Trivy, CodeQL. *(UMS/Tracker partially EXISTS)* |
| G1 — Integration | nightly, ephemeral kind (substrate + umbrella) | staging | **matrix F1–F7 automated** + assertions the critics demanded: consumer endpoint *started* (bus health, not just pod Ready), InboxState row written on consume, an allowed AND a denied NetworkPolicy path |
| G2 — Staging soak | ≥24 h per RC | RC candidacy | R1–R6 resilience (broker kill → outbox drains; consumer kill → catch-up; poison → `_error` → reprocess), P1–P3 perf budgets, 24 h zero-drift reconciliation, dashboards+alerts live |
| G3 — RC Stamped | `evolith-cli gate evaluate -p qa` (gate-F4) | prod PR | Test Summary, Acceptance, Security scan, Integration evidence, Pyramid — the F4 stamp is a required check on the prod fleet-repo PR |
-| G4 — Post-deploy | prod, after Flux reconcile | marks healthy / triggers rollback | smoke: health all pods; synthetic tenant create → projection visible in UMS+Tracker within lag SLO → deactivate; `_error` depth unchanged; 30-min error-rate window |
+| G4 — Post-deploy | prod, after Flux reconcile | marks healthy / triggers rollback | smoke: health all pods; synthetic tenant create in UMS → projection visible in the Tracker within lag SLO → deactivate; `_error` depth unchanged; 30-min error-rate window |
-Hard rules: contract migrations never ship with features · consumer-first ordering for additive changes · no prod promotion of tenant-projection features until the ownership migration (M-phases) is scheduled.
+Hard rules: contract migrations never ship with features · consumer-first ordering for additive changes · the tenant write split (§12) is an invariant, not a milestone — a governed action that writes `code`/`name`/`status` in the Tracker is a regression whatever else is green.
## 14. Observability
Stack (ns `observability`, BUILD — configs already exist under `product/operations/`, nothing ships them yet): kube-prometheus-stack + Loki (single-binary) + Tempo + OTel Collector; Grafana provisioning, Prometheus alerts, Tempo config reused from `product/operations/{grafana,alerts,otel,tempo}`. VPS profile: single-replica, 7d metrics/3d traces; AKS: ZRS PVCs, 30d.
-Per-product code deltas (BUILD, prerequisites for G2): MMS has zero OTel; UMS `ObservabilityExtensions` misses `AddSource("MassTransit")` (consumer spans invisible); Tracker consumer is ILogger-only. Standard meters: `masterdata_projection_applied/discarded_total`, `consumer lag`, `_error` queue depth, e2e latency histogram (per the canonical design §11).
+Per-product code deltas (BUILD, prerequisites for G2): UMS `ObservabilityExtensions` misses `AddSource("MassTransit")` (producer/consumer spans invisible); the Tracker consumer is ILogger-only. Meters to ADD — none of these has an emitter today, and the `masterdata_projection_applied/discarded_total` pair this plan used to name never had one either: applied/discarded counters on the snapshot consumer, consumer lag, `_error` queue depth, e2e latency histogram. Until they exist, queue depth and the `ums_projection_version` column are the only honest freshness signals (see the reprocess runbook).
## 15. Consolidated risk register (deduplicated, verified)
+Rows 1, 4, 5 and 7 were closed on 2026-08-22 by ADR-UMS-107 + T-059, and rows 6, 11 and 14 were MMS's — they leave with the product. They are listed as struck rather than deleted, because a risk register that quietly loses rows cannot be audited.
+
| # | Risk | Sev | Owner | Mitigation | Phase |
|---|---|---|---|---|---|
-| 1 | Two-writer tenant ownership (UMS/Tracker still author) | Critical | Winston | M0–M4 ladder (§12) | M-phases |
+| 1 | ~~Two-writer tenant ownership (UMS/Tracker both author)~~ | Critical -> **closed** | Winston | T-059 split the writes: UMS owns `code`/`name`/`status` and existence; the Tracker keeps `tier`, `contact_email`, `settings`, localisation (§12) | done 2026-08-22 |
| 2 | ~~Consistent-hash exchange splits traffic between consumers~~ **fixed by §5.2 decision** | Critical -> resolved | Arch | MassTransit-owned fanout topology | done in doc; CRD retirement BUILD |
-| 3 | CRD/code queue-declare conflict (406 → silent dead consumer) | Critical | Arch | retire queue CRDs (§5.2); G1 asserts endpoint *started* | M0 |
-| 4 | Inbox dedup not wired in consumers | Major | DevOps | `UseEntityFrameworkOutbox` on endpoints (§5.5-1) + G1 InboxState assert | M0 |
-| 5 | Projection concurrency race (permanent regression) | Major | DevOps | conditional set-based upsert (§5.5-2) | M0 |
-| 6 | MMS probes require Development env → prod CrashLoop | Major | Infra | real `/health` endpoints before first staging deploy | pre-staging |
-| 7 | `Default`/`DefaultConnection` fallback → consumers silently on localhost | Major | Infra | fix fallback + explicit `MasterDataDb` in charts | M0 |
-| 8 | Startup migrations race at replicas>1 | Major | Infra | migrate-Job pattern suite-wide | pre-staging |
-| 9 | Poison-message alerts watching the wrong queue (DLX vs `_error`) | Major | Infra | `_error`-depth alerts + shovel runbook (§5.3) | pre-staging |
+| 3 | CRD/code queue-declare conflict (406 → silent dead consumer) | Critical | Arch | retire queue CRDs (§5.2); G1 asserts endpoint *started* | pre-staging |
+| 4 | ~~Inbox dedup not wired in consumers~~ | Major -> **closed** | DevOps | `UseEntityFrameworkOutbox` on the endpoint in `TenantSnapshotConsumerDefinition` (§5.5-1); G1 keeps the InboxState assert | done 2026-08-22 |
+| 5 | ~~Projection concurrency race (permanent regression)~~ | Major -> **closed** | DevOps | version-guarded set-based upsert (§5.5-2) | done 2026-08-22 |
+| 6 | ~~MMS probes require Development env → prod CrashLoop~~ | — | — | the product does not exist | withdrawn 2026-08-23 |
+| 7 | ~~`Default`/`DefaultConnection` fallback → consumers silently on localhost~~ | Major -> **moot** | Infra | T-059 removed the separate `MasterDataDb` context entirely | done 2026-08-22 |
+| 8 | Startup migrations race at replicas>1 | Major | Infra | migrate-Job pattern suite-wide (§5.5-3) | pre-staging |
+| 9 | Poison-message alerts watching the wrong queue | Major | Infra | `_error`-depth alert on `tracker.tenant-snapshot_error` + shovel runbook (§5.3). The alerts named two MMS-era queues until 2026-08-22; `TenantProjectionQueueMissing` would have fired critical forever | done in doc; deploy BUILD |
| 10 | Broker = shared critical dependency | Major | Infra | outbox (proven) + quorum where ≥3 nodes + freshness-only degradation (§5.6) | standing |
-| 11 | Plaintext creds in mms-helm values; shared default-user | Minor | Infra | per-product broker users (CRDs) + ESO/AKV | pre-staging |
-| 12 | Contract drift across 3 TenantEvent copies | Minor | DevOps | `Evolith.Messaging.Contracts` + G0 contract tests | M0–M1 |
+| 11 | ~~Plaintext creds in mms-helm values~~; shared default-user | Minor | Infra | per-product broker users (CRDs) + ESO/AKV — the mms-helm half is withdrawn with the product | pre-staging |
+| 12 | Contract drift across the copies of the tenant contract | Minor | DevOps | two verbatim copies today (UMS, Tracker) because `Unimar.Ums.Sdk.Contracts` was never published; publish it + G0 contract tests (§11) | standing |
| 13 | kind CNI doesn't enforce NetworkPolicies (parity trap) | Minor | Infra | Cilium on kind + G1 allow/deny assertions | pre-staging |
-| 14 | MMS tenant CRUD has no authN | Minor | Arch | authN before any ingress exposure | pre-staging |
-| 15 | Consumer behavior unobservable (no MassTransit traces/meters) | Minor | Infra | §14 code deltas as G2 prerequisite | pre-staging |
+| 14 | ~~MMS tenant CRUD has no authN~~ | — | — | the product does not exist | withdrawn 2026-08-23 |
+| 15 | Consumer behavior unobservable (no MassTransit traces/meters) | Minor | Infra | §14 code deltas as G2 prerequisite — and note §14's meters do not exist yet, under any name | pre-staging |
## 16. Prioritized BUILD backlog
-**P0 (blocks any staging):** retire message-path CRDs + per-product broker Users/Permissions · §5.5 consumer fixes (inbox wiring, conditional upsert, migrate-Jobs, `Default` fallback) · MMS health endpoints · commit UMS DI gating · MMS CI pipeline · explicit `MasterDataDb`/`RabbitMq` in ums/tracker charts.
-**P1 (blocks prod):** `Evolith.Messaging.Contracts` package + contract tests · observability stack deploy + code deltas · NetworkPolicies + Quotas + Cilium-on-kind · secrets to ESO/OpenBao · Flux bootstrap + `clusters/*` overlays + orderable image tags · G1 automation (matrix F1–F7 + the three new assertions) · CNPG + off-node backups · rebuild ums-helm on Tracker template set · re-home Core charts to `evolith-core` ns values.
+**P0 (blocks any staging):** retire message-path CRDs + per-product broker Users/Permissions · the one §5.5 item still open (migrate-Jobs at replicas>1) · commit UMS DI gating · explicit `RabbitMq` in the ums/tracker charts.
+**P1 (blocks prod):** publish the shared contracts package so `TenantSnapshotIntegrationEvent` stops living in two copies, + contract tests · observability stack deploy + code deltas · NetworkPolicies + Quotas + Cilium-on-kind · secrets to ESO/OpenBao · Flux bootstrap + `clusters/*` overlays + orderable image tags · G1 automation (matrix F1–F7 + the three new assertions) · CNPG + off-node backups · rebuild ums-helm on Tracker template set · re-home Core charts to `evolith-core` ns values.
**P2 (hardening/growth):** AKS overlay (Terraform/Bicep + AKV CSI + Flexible Servers) · umbrella chart local E2E · pod hardening (non-root, RO rootfs) · SLO dashboards → revisit canary · external-dns · weekly `_error` review task.
-## 17. How-to (16 steps)
+## 17. How-to
| # | Step | Commands / assets |
|---|---|---|
@@ -317,14 +315,13 @@ Per-product code deltas (BUILD, prerequisites for G2): MMS has zero OTel; UMS `O
| 2 | Namespaces | `kubectl apply -f deploy/kubernetes/namespaces.yaml` |
| 3 | Broker | Cluster-Operator install; `kubectl apply -f deploy/kubernetes/messaging/rabbitmq-cluster.yaml` (+ replicas overlay); per-product `User`/`Permission` CRDs (BUILD) |
| 4 | Core | re-valued `evolith-core-api|mcp|agent-runtime` charts (`product/infra/helm/`, `local-test.sh` builds/loads) → ns `evolith-core` |
-| 5 | MMS | `helm upgrade -i mms mms/infra/mms-helm -n mms -f values-local.yaml` (after health-endpoint fix) |
-| 6 | UMS | rebuilt ums-helm (Tracker template set) with `ConnectionStrings__RabbitMq` + `__MasterDataDb`; DI gating committed |
-| 7 | Tracker | `evolith_tracker/product/infra/helm/*` charts + the two connection keys in secret.yaml |
+| 5 | UMS | rebuilt ums-helm (Tracker template set) with `ConnectionStrings__RabbitMq`; DI gating committed |
+| 6 | Tracker | `evolith_tracker/product/infra/helm/*` charts + the connection keys in secret.yaml |
| 8 | Ingress/DNS/TLS | pinned Traefik chart + cert-manager + IngressRoutes; `/etc/hosts` `*.evolith.local` |
| 9 | Databases | per-product Postgres (StatefulSet local); run migrate-Jobs |
| 10 | Secrets/config | local plain Secrets; standardized connection keys; fallback bug fixed |
| 11 | Observability | new `deploy/kubernetes/observability/` shipping kube-prometheus-stack/Loki/Tempo/OTel from `product/operations/` configs |
-| 12 | Integration tests | automate matrix F1–F7 (projection rows in UMS+Tracker DBs) |
+| 12 | Integration tests | automate matrix F1–F7 (the projection row in the Tracker DB, with its `ums_projection_version`) |
| 13 | RabbitMQ validation | R1–R6: broker kill → outbox drains; consumer kill → catch-up; poison → `_error` → shovel back; mgmt UI :30672 |
| 14 | Pre-staging gates | matrix Musts green + dashboards + alerts + 24 h zero-drift |
| 15 | **Staging promotion** | 15a k3s on VPS (or Coolify-managed K8s); 15b Flux bootstrap (`flux bootstrap github …`), apply `clusters/staging/`; 15c ESO + OpenBao (`ClusterSecretStore` + `ExternalSecret` per ns); 15d LE **staging** issuer + `*.stg` DNS; 15e values-staging (replicas 1, quotas §4.3); 15f run G2 soak checklist |
@@ -332,4 +329,4 @@ Per-product code deltas (BUILD, prerequisites for G2): MMS has zero OTel; UMS `O
## 18. References
-ADR-0106 · ADR-0107 · ADR-0033 · `mms/docs/architecture/tenant-master-data-projection.md` (canonical flow) + `tenant-projection-test-matrix.md` · `deploy/kubernetes/README.md` (substrate bring-up) · `product/infra/deployment-topology.md` (canonical name map) · `product/infra/helm/README.md` (Core charts) · `product/infra/vps-coolify/README.md` · `product/operations/` (alerts, grafana, otel, tempo, load-tests, incident-response runbooks) · UMS ADR-0083 · Tracker ADR T-037.
+ADR-0129 · ADR-0107 · ADR-0033 · `deploy/kubernetes/README.md` (substrate bring-up) · `product/infra/deployment-topology.md` (canonical name map) · `product/infra/helm/README.md` (Core charts) · `product/infra/vps-coolify/README.md` · `product/operations/` (alerts, grafana, otel, tempo, load-tests, incident-response runbooks) · ADR-UMS-107 (UMS publishes the tenant) · Tracker ADR T-059 (the Tracker projects it) · Tracker ADR T-037.