Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/architecture/adrs/0055-graphql-rest-hybrid-api.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

| Campo | Valor |
|---|---|
| **Estado** | Aceptado |
| **Estado** | **Reemplazado por [ADR-0166](./0166-retire-graphql-single-rest-surface.es.md)** (2026-08-10) |
| **Fecha** | 2026-05-21 |
| **Contexto** | UMS Web App — Estrategia de Comunicación de API |
| **Decisores** | Equipo de Arquitectura |
Expand Down Expand Up @@ -96,6 +96,11 @@ La separación de niveles permanece como el camino de escalada correcto si estos

---

> **Reemplazado.** GraphQL se retiró de UMS el 2026-08-10: el esquema, el endpoint
> `/graphql` y los 17 tipos de consulta ya no existen. La superficie REST cubría el 100% de
> las lecturas y era la única que los clientes usaban. Ver ADR-0166 para la medición que lo
> sustenta. Este documento se conserva como registro de lo que se decidió y por qué.

### Alternativa 2: GraphQL para Queries y Mutaciones

Usar GraphQL exclusivamente — queries y mutaciones — eliminando la capa REST.
Expand Down
7 changes: 6 additions & 1 deletion docs/architecture/adrs/0055-graphql-rest-hybrid-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| Field | Value |
|---|---|
| **Status** | Accepted |
| **Status** | **Superseded by [ADR-0166](./0166-retire-graphql-single-rest-surface.md)** (2026-08-10) |
| **Date** | 2026-05-21 |
| **Context** | UMS Web App — API Communication Strategy |
| **Deciders** | Architecture Team |
Expand Down Expand Up @@ -98,6 +98,11 @@ Tier separation remains the correct escalation path if these controls prove insu

---

> **Reemplazado.** GraphQL se retiró de UMS el 2026-08-10: el esquema, el endpoint
> `/graphql` y los 17 tipos de consulta ya no existen. La superficie REST cubría el 100% de
> las lecturas y era la única que los clientes usaban. Ver ADR-0166 para la medición que lo
> sustenta. Este documento se conserva como registro de lo que se decidió y por qué.

### Alternative 2: GraphQL for Both Queries and Mutations

Use GraphQL exclusively — queries and mutations — eliminating the REST layer.
Expand Down
12 changes: 9 additions & 3 deletions docs/architecture/adrs/0059-single-api-tier-decision.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
| Field | Value |
|---|---|
| **ID** | ADR-0059 |
| **Status** | ACCEPTED |
| **Status** | ACCEPTED — **amended by [ADR-0166](./0166-retire-graphql-single-rest-surface.md)** (2026-08-10) |
| **Date** | 2026-05-15 |
| **Deciders** | Architecture Team |
| **Evolith Relationship** | Override — UMS diverges from Evolith multi-tier API baseline |
Expand All @@ -20,9 +20,15 @@ Evolith permits splitting query and command surfaces into separate API tiers whe

## Decision

UMS will co-locate GraphQL (query) and REST (command) surfaces in a **single `ums.api` deployment unit** using HotChocolate Minimal APIs on .NET 10.
UMS will serve its API from a **single `ums.api` deployment unit** on .NET 10.

CQRS separation is maintained at the **protocol level** (GraphQL for reads, REST for writes) but not at the deployment/infrastructure level.
> **Amended by ADR-0166 (2026-08-10).** As written, this decision co-located a GraphQL query
> surface and a REST command surface. GraphQL has since been retired: there is one surface, REST,
> and nothing to co-locate. What survives — and is the reason this ADR stands — is the rejection of
> splitting reads and writes into **separately deployed tiers**, which ADR-0166 does not revisit.

CQRS separation is maintained, but no longer at the protocol level: it lives in code organisation
(`Endpoints/**/Queries/` for reads, `Endpoints/**/` for commands) over MediatR's `IQuery`/`ICommand`.

## Rationale

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
**Disposicion Evolith:** Propuesto para adopcion como estandar universal de Evolith
**Relacionados:**
- [ADR-0053: Observabilidad con OpenTelemetry](./0053-opentelemetry-observability.md)
- [ADR-0055: Patron Hibrido GraphQL/REST](./0055-graphql-rest-hybrid-api.md)
- [ADR-0055: Patron Hibrido GraphQL/REST](./0055-graphql-rest-hybrid-api.es.md) — *reemplazado por [ADR-0166](./0166-retire-graphql-single-rest-surface.es.md)*
- [ADR-0057: Estado con Zustand y TanStack Query](./0057-zustand-tanstack-query-state.md)
- [ADR-0061: Execution Context Accessor](./0061-execution-context-accessor.es.md)
- [ADR-0062: Configuracion Serilog Segura de PII](./0062-pii-safe-serilog-configuration.es.md)
Expand Down Expand Up @@ -73,6 +73,12 @@ La implementacion actual de UMS produce `userMessage` mediante recursos de local

### 3. Limite GraphQL

> **Ya no aplica (2026-08-10).** GraphQL se retiró de UMS por
> [ADR-0166](./0166-retire-graphql-single-rest-surface.es.md): no hay esquema, ni endpoint
> `/graphql`, ni resolvers. Esta cláusula se conserva como registro de lo que el contrato exigía
> mientras esa superficie existió, y como especificación a respetar si algún día se reintroduce.
> El resto del ADR sigue vigente: trata del contrato de error REST, que ahora es el único.

GraphQL se usa para lecturas segun ADR-0055. Los errores GraphQL expuestos al cliente usan mensajes genericos seguros localizados y transportan `errorId`. Las excepciones de resolvers se registran mediante Serilog con el mismo `errorId`, pero no se serializan al navegador.

### 4. Observabilidad y Logging
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
**Evolith disposition:** Proposed for adoption as a universal Evolith standard
**Related:**
- [ADR-0053: OpenTelemetry Observability](./0053-opentelemetry-observability.md)
- [ADR-0055: GraphQL/REST Hybrid API Pattern](./0055-graphql-rest-hybrid-api.md)
- [ADR-0055: GraphQL/REST Hybrid API Pattern](./0055-graphql-rest-hybrid-api.md) — *superseded by [ADR-0166](./0166-retire-graphql-single-rest-surface.md)*
- [ADR-0057: Zustand and TanStack Query State Management](./0057-zustand-tanstack-query-state.md)
- [ADR-0061: Execution Context Accessor](./0061-execution-context-accessor.md)
- [ADR-0062: PII-Safe Serilog Configuration](./0062-pii-safe-serilog-configuration.md)
Expand Down Expand Up @@ -75,6 +75,12 @@ The current UMS implementation produces `userMessage` through API localization r

### 3. GraphQL Boundary

> **No longer applicable (2026-08-10).** GraphQL was retired from UMS by
> [ADR-0166](./0166-retire-graphql-single-rest-surface.md); there is no schema, no `/graphql`
> endpoint and no resolvers. This clause is kept as the record of what the contract required while
> that surface existed — and as the specification to honour if it is ever reintroduced. Everything
> else in this ADR stands: it is about the REST error contract, which is now the only one.

GraphQL is used for reads according to ADR-0055. GraphQL errors exposed to clients use safe localized generic messages and carry `errorId`. Resolver exceptions are logged through Serilog with that same `errorId`, but are not serialized to the browser.

### 4. Observability and Logging
Expand Down
108 changes: 108 additions & 0 deletions docs/architecture/adrs/0166-retire-graphql-single-rest-surface.es.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# ADR-0166: Retirar GraphQL — REST Como Única Superficie

**Estado:** Aceptado
**Fecha:** 2026-08-10
**Responsable de Decisión:** Arquitectura
**Reemplaza:** [ADR-0055](./0055-graphql-rest-hybrid-api.es.md) — patrón híbrido GraphQL/REST
**Modifica:** [ADR-0059](./0059-single-api-tier-decision.md) — su decisión era co-ubicar ambas superficies; queda una sola
**Cierra:** TD-002

---

## Contexto

ADR-0055 estableció el patrón híbrido —GraphQL para lecturas, REST para escrituras— y ADR-0059
decidió co-ubicar ambas superficies en un solo despliegue. Los dos siguen en estado Aceptado.

El código dice otra cosa desde hace meses:

| Superficie | Estado medido (2026-08-10) |
| --- | --- |
| Servidor | 17 clases de consulta registradas, esquema vivo con 39 campos |
| Aplicación web — módulos GraphQL | 9, de los cuales **uno** tenía consumidor |
| Aplicación web — los otros ocho | Código muerto: existían con pruebas y nadie los llamaba |
| SDK (.NET y TypeScript) | No usa GraphQL en absoluto |
| Pruebas E2E | Tampoco |

El único llamante vivo era `branding.service.getBranding`. Todo lo demás pasó a REST en la
resincronización de 2026-08-09, que se llevó el parámetro `FRONTEND_CONFIG_TRANSPORT` y el
`query-transport.service` que decidía el transporte: la capa de acceso a datos importada llama a
REST directamente.

### La decisión ya estaba tomada: D-007

Esto **no es una decisión nueva**. El 2026-07-14, D-007 estableció «API REST-only con CQRS en la
capa de aplicación», revisando expresamente ADR-0055 y ADR-0059. La documentación de referencia lo
recogió entonces y sigue diciéndolo hoy: la arquitectura de solución afirma que «GraphQL fue
retirado», los objetivos de calidad cambiaron su SLO de lectura de GraphQL a `GET`, el modelo de
amenazas retiró el vector de DoS por GraphQL, y el PRD declara la API REST-only.

Lo que nunca ocurrió fue el cambio en el código. Un mes después seguían en pie el esquema, el
endpoint `/graphql`, el paquete y las 17 clases; y ADR-0055 y ADR-0059 seguían en estado Aceptado,
contradiciendo a D-007 sin que nada lo señalara.

> **Por qué se pasó por alto.** El registro de decisiones vive en un `DECISIONS.md` que **no está
> en este repositorio**: los 117 enlaces a `../../DECISIONS.md` y `../../GAPS.md` que hay en
> `docs/` apuntan a la nada. Quien busque «¿se decidió algo sobre GraphQL?» mirando los ADRs
> —donde 0055 y 0059 dicen Aceptado— concluye que no. Eso es exactamente lo que ocurrió al abrir
> este trabajo, y lo corrigió el propio texto de `solution-architecture.es.md`. La deuda de tener
> el registro fuera del repositorio queda anotada aparte.

Así que este ADR hace dos cosas: **implementa D-007** en el código, y **escribe el registro ADR
que D-007 necesitaba** para que ADR-0055 y ADR-0059 dejen de decir lo contrario.

Mantener un tier de consulta completo para un solo llamante, además, tiene coste y no rinde: una
dependencia más (HotChocolate), una superficie más que asegurar y versionar, un filtro de errores
propio, un interceptor de contexto de inquilino, y 17 clases que hay que mantener alineadas con sus
queries cuando cambia un DTO.

## Decisión

### §2.1 — Se retira GraphQL por completo

Desaparecen el esquema, el endpoint `/graphql` (raíz y versionado), el paquete
`HotChocolate.AspNetCore`, las 17 clases de consulta, `SafeGraphQlErrorFilter`,
`TenantContextGraphQlInterceptor` y los helpers `UnwrapGraphQl`/`UnwrapGraphQlOrNull`. En el
cliente desaparecen `graphqlClient` y los nueve módulos `*.graphql.ts`.

### §2.2 — REST ya cubría el 100%, y eso es lo que hace la retirada segura

No se ha escrito ni un endpoint nuevo. Existe una carpeta `Endpoints/**/Queries/` paralela con
**22 grupos REST de consulta** frente a las 17 clases GraphQL, y cada consulta del esquema tiene
gemela que envía **la misma query de MediatR**. La lectura de identidad visual, por ejemplo, ya
estaba en `Queries/BrandingQueryEndpoints` desde antes; el resolver y el endpoint enviaban ambos
`GetBrandingByTenantIdQuery`.

> Esa carpeta paralela es también el motivo por el que un primer sondeo de este trabajo concluyó
> —mal— que había que añadir un GET: se miró el fichero de comandos y no el de consultas. La
> comprobación buena es enumerar las dos superficies y casarlas una a una, y se hizo.

### §2.3 — La separación CQRS se mantiene; deja de ser por protocolo

ADR-0059 mantenía la separación lectura/escritura **a nivel de protocolo** (GraphQL vs REST). Esa
frontera se mueve a la organización del código: `Endpoints/**/Queries/` para lecturas,
`Endpoints/**/` para comandos, sobre `IQuery`/`ICommand` de MediatR. La separación que importa
—qué handler puede escribir— nunca dependió del transporte.

## Consecuencias

- Una dependencia menos y una superficie de ataque menos. El endpoint `/graphql` era anónimo por
construcción salvo por el interceptor de inquilino; deja de existir.
- `branding.service.getBranding` colapsa a `null` tanto el 200 con cuerpo vacío (el inquilino
existe y no tiene identidad visual) como el 404 (el inquilino no existe), que es exactamente lo
que hacía el camino GraphQL. Cualquier otro error se propaga.
- TD-002 —«la consulta `appConfigurations` devuelve vacío»— se cierra por desaparición del sujeto.
Nunca se encontró la causa raíz y llevaba meses sin síntoma porque nada enrutaba por ahí.
- El blueprint TE-07 del gateway YARP pierde su ruta `/graphql`.
- **Si en el futuro se quiere GraphQL**, este ADR no lo prohíbe: dice que hoy no lo paga nadie.
Reintroducirlo sería una decisión nueva, con un consumidor real que la justifique.

## Alternativas descartadas

- **Volver a usarlo de verdad**, como decían los ADRs. Implicaría reconectar la capa de datos
importada, que es REST-only, para ganar qué: la superficie REST ya cubre el 100% de las lecturas
y el producto funciona. Sería trabajo para volver al punto de partida.
- **Dejarlo como estaba.** Es lo más barato hoy y lo más caro después: dos ADRs Aceptados
describiendo una arquitectura que no existe, y 17 clases que alguien seguirá manteniendo por
disciplina sin que nadie las llame. La documentación que miente cuesta más que el código que
sobra.
105 changes: 105 additions & 0 deletions docs/architecture/adrs/0166-retire-graphql-single-rest-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# ADR-0166: Retire GraphQL — REST as the Only Surface

**Status:** Accepted
**Date:** 2026-08-10
**Decision Owner:** Architecture
**Supersedes:** [ADR-0055](./0055-graphql-rest-hybrid-api.md) — GraphQL/REST hybrid pattern
**Amends:** [ADR-0059](./0059-single-api-tier-decision.md) — its decision was to co-locate both surfaces; one remains
**Closes:** TD-002

---

## Context

ADR-0055 established the hybrid pattern — GraphQL for reads, REST for writes — and ADR-0059 decided
to co-locate both surfaces in a single deployment. Both are still Accepted.

The code has said otherwise for months:

| Surface | Measured state (2026-08-10) |
| --- | --- |
| Server | 17 query classes registered, live schema with 39 fields |
| Web app — GraphQL modules | 9, of which **one** had a consumer |
| Web app — the other eight | Dead code: they existed with tests and nothing called them |
| SDK (.NET and TypeScript) | Does not use GraphQL at all |
| E2E tests | Neither |

The only live caller was `branding.service.getBranding`. Everything else moved to REST in the
2026-08-09 resync, which removed the `FRONTEND_CONFIG_TRANSPORT` parameter and the
`query-transport.service` that chose the transport: the imported data-access layer calls REST
directly.

### The decision was already made: D-007

This is **not a new decision**. On 2026-07-14, D-007 established "REST-only API with CQRS in the
application layer", expressly revising ADR-0055 and ADR-0059. The reference documentation recorded
it then and still says so today: the solution architecture states that "GraphQL was retired", the
quality objectives moved their read SLO from GraphQL to `GET`, the threat model dropped the
GraphQL DoS vector, and the PRD declares the API REST-only.

What never happened was the code change. A month later the schema, the `/graphql` endpoint, the
package and the 17 classes were all still standing; and ADR-0055 and ADR-0059 were still Accepted,
contradicting D-007 with nothing to flag it.

> **Why it was missed.** The decision register lives in a `DECISIONS.md` that is **not in this
> repository**: the 117 links to `../../DECISIONS.md` and `../../GAPS.md` across `docs/` point at
> nothing. Anyone asking "was anything decided about GraphQL?" by reading the ADRs — where 0055 and
> 0059 say Accepted — concludes no. That is exactly what happened when this work opened, and what
> corrected it was the prose of `solution-architecture.es.md`. The debt of keeping the register
> outside the repository is recorded separately.

So this ADR does two things: it **implements D-007** in code, and it **writes the ADR record D-007
needed** so that ADR-0055 and ADR-0059 stop saying the opposite.

Keeping a whole query tier for one caller also costs and returns nothing: one more dependency
(HotChocolate), one more surface to secure and version, a bespoke error filter, a tenant-context
interceptor, and 17 classes that must be kept aligned with their queries whenever a DTO changes.

## Decision

### §2.1 — GraphQL is retired in full

The schema, the `/graphql` endpoint (root and versioned), the `HotChocolate.AspNetCore` package,
the 17 query classes, `SafeGraphQlErrorFilter`, `TenantContextGraphQlInterceptor` and the
`UnwrapGraphQl`/`UnwrapGraphQlOrNull` helpers all go. On the client, `graphqlClient` and the nine
`*.graphql.ts` modules go with them.

### §2.2 — REST already covered 100%, and that is what makes the removal safe

Not one new endpoint was written. A parallel `Endpoints/**/Queries/` folder holds **22 REST query
groups** against GraphQL's 17 query classes, and every schema query has a twin dispatching **the
same MediatR query**. Branding reads, for instance, already lived in
`Queries/BrandingQueryEndpoints`; resolver and endpoint both sent `GetBrandingByTenantIdQuery`.

> That parallel folder is also why a first survey of this work concluded — wrongly — that a GET had
> to be added: it looked at the commands file and not the queries one. The correct check is to
> enumerate both surfaces and match them one to one, and that was done.

### §2.3 — CQRS separation stays; it stops being protocol-level

ADR-0059 kept the read/write split **at the protocol level** (GraphQL vs REST). That boundary moves
into code organisation: `Endpoints/**/Queries/` for reads, `Endpoints/**/` for commands, over
MediatR's `IQuery`/`ICommand`. The separation that matters — which handler may write — never
depended on the transport.

## Consequences

- One dependency fewer and one attack surface fewer. The `/graphql` endpoint was anonymous by
construction apart from the tenant interceptor; it ceases to exist.
- `branding.service.getBranding` collapses to `null` both the 200 with an empty body (the tenant
exists and has no branding) and the 404 (the tenant does not exist), which is exactly what the
GraphQL path did. Any other error propagates.
- TD-002 — "the `appConfigurations` query returns empty" — closes because its subject is gone. The
root cause was never found and it had been symptomless for months because nothing routed there.
- The TE-07 YARP gateway blueprint loses its `/graphql` route.
- **If GraphQL is wanted again in future**, this ADR does not forbid it: it says nobody is paying
for it today. Reintroducing it would be a new decision, with a real consumer to justify it.

## Alternatives rejected

- **Actually start using it**, as the ADRs said. That would mean rewiring the imported data-access
layer, which is REST-only, to gain what: the REST surface already covers 100% of reads and the
product works. Work to get back to the starting point.
- **Leave it as it was.** Cheapest today and dearest later: two Accepted ADRs describing an
architecture that does not exist, and 17 classes someone keeps maintaining out of discipline
while nothing calls them. Documentation that lies costs more than code that is surplus.
Loading
Loading