Jesús Erro
Explorar conexiones
Camino read-only hacia un gate de escritura controlada con identidad, ETag, mínimo privilegio y audit sobre Azure SQL DEV.

Del read-only al primer writer controlado en IXATU Operations

Este artículo es la narrativa pública del documento de trabajo interno correspondiente. Conserva hallazgos, semántica, fallos reales y límites; omite o generaliza únicamente identificadores operativos privados que no aportan valor editorial.

IXATU Operations ha cruzado la frontera entre consultar el sistema legacy y modificarlo de forma controlada.

La primera capability de escritura se ha implementado deliberadamente sobre un único campo —Contactos.[Tfno principal]— para demostrar el patrón completo: autenticación, autorización, separación reader/writer, permisos SQL mínimos, concurrencia optimista, transacción, auditoría, UI humana, CI/CD y aceptación real en IXATU_DEV.

PROD permanece sin writer configurado.

El valor estratégico de v0.9.0 no es «poder cambiar un teléfono», sino disponer del primer carril seguro y reusable de escritura de Operations sobre el sistema actual.

Índice

  1. [[#1. Estado ejecutivo]]
  2. [[#2. Contexto: de lectura a escritura]]
  3. [[#3. Qué se ha construido en v0.9.0]]
  4. [[#4. Por qué empezar por un único campo]]
  5. [[#5. Discovery LPD y contrato legacy]]
  6. [[#6. Arquitectura del writer]]
  7. [[#7. Capas de seguridad]]
  8. [[#8. Autenticación, autorización y permisos SQL no son lo mismo]]
  9. [[#9. Alta de usuarios autorizados en DEV]]
  10. [[#10. Reader y writer SQL]]
  11. [[#11. Concurrencia: rowversion, ETag e If-Match]]
  12. [[#12. Transacción y auditoría]]
  13. [[#13. Semántica del teléfono y Sin informar]]
  14. [[#14. Entornos: localhost, DEV y PROD]]
  15. [[#15. Flujo de implementación, merge, Bake y Deploy DEV]]
  16. [[#16. Evidencias y pruebas realizadas]]
  17. [[#17. Hallazgos y decisiones significativas]]
  18. [[#18. Patrón reusable para futuros writers]]
  19. [[#19. Qué no debemos generalizar todavía]]
  20. [[#20. Roadmap global recorrido]]
  21. [[#21. Seis frentes aprobados desde este punto]]
  22. [[#22. Frente 2 — próximo objetivo: formulario Contacto]]
  23. [[#23. Evolución hacia operaciones multisuministro]]
  24. [[#24. CLI y agentes IA]]
  25. [[#25. Paso futuro a PROD]]
  26. [[#26. Reglas operativas y mnemotécnicas]]
  27. [[#27. Riesgos y deuda conocida]]
  28. Evidencia operativa (sin identificadores privados)

1. Estado ejecutivo

A fecha 2026-09-18, IXATU Operations dispone en DEV de su primer writer humano operativo end-to-end.

La capability permite editar únicamente:

dbo.Contactos.[Tfno principal]

El flujo se ha probado con un contacto de prueba real de IXATU_DEV (un contacto de prueba de DEV) y se ha validado:

  • login mediante Microsoft Entra ID;
  • autorización por Object ID;
  • acceso Web → API mediante gate interno;
  • GET fresco del valor y su versión;
  • PATCH protegido por If-Match;
  • escritura real en Azure SQL DEV;
  • rechazo de una escritura obsoleta mediante HTTP 412;
  • auditoría sin almacenar PII;
  • restauración exacta del valor original;
  • UI humana con Editar, Guardar, Cancelar y gestión de conflicto;
  • despliegue automatizado de DEV desde main;
  • acceso de prueba habilitado para el equipo mediante allowlist.

Estado de entornos:

Área DEV PROD
Lectura Operations Operativa Operativa
Entra / autenticación Operativa Operativa
Writer SQL Operations No
UI de edición teléfono No promovida/configurada para escritura
Allowlist de writers No
Audit de writes No aplica
Escrituras Operations Permitidas y controladas Bloqueadas por diseño
Fase actual Estabilización / aceptación Lectura / sin writer

La regla vigente es:

PROD_MUTATIONS = 0

No se configura ni se habilita escritura en producción durante el Release Gate de v0.9.0.


2. Contexto: de lectura a escritura

Operations se diseñó para sustituir Access de forma incremental, sin migrar primero todo el modelo legacy.

Principio central:

Primero sustituir el comportamiento de Access; después sustituir su modelo de datos.

Durante las versiones anteriores, Operations fue ganando capacidad de lectura sobre el sistema real:

flowchart LR
    A[Access + Azure SQL legacy] --> B[ACL / adapters]
    B --> C[FastAPI]
    C --> D[Operations Web]
    D --> E[Usuarios]

Hasta v0.8.x, la dirección era fundamentalmente:

Azure SQL

Operations

Usuario

Con v0.9.0 aparece por primera vez el camino inverso:

Usuario

Operations

Azure SQL DEV

Esto cambia la naturaleza del producto.

Operations deja de ser únicamente una interfaz de consulta y se convierte en una aplicación capaz de ejecutar operaciones de negocio controladas sobre el legacy actual.


3. Qué se ha construido en v0.9.0

La implementación se dividió deliberadamente en dos slices.

Slice A — Write Foundation + Backend

Issues / PRs:

  • la Issue correspondiente
  • la PR correspondiente

Objetivo:

  • construir el carril de escritura;
  • mantener separado el read-side existente;
  • habilitar autenticación/autorización;
  • crear un principal SQL writer mínimo;
  • implementar concurrencia y audit;
  • validar una escritura real en DEV.

Resultado:

backend + seguridad + SQL + audit + deployment = PASS

Slice B — UI humana

Issues / PRs:

  • la Issue correspondiente
  • la PR correspondiente

Objetivo:

  • exponer el writer desde la ficha de cliente;
  • no convertir toda la ficha en formulario;
  • reutilizar exactamente la capability backend;
  • validar la experiencia humana.

Resultado:

UI → API → writer → IXATU_DEV = PASS

Release Gate actual

Issue:

  • #79 — v0.9.0 Release Gate — estabilización y aceptación de equipo en DEV

En esta fase:

  • no se añaden nuevas capabilities;
  • el equipo prueba el writer en DEV;
  • se recogen incidencias;
  • sólo se corrigen defectos bloqueantes o pequeñas fricciones de v0.9.0;
  • PROD permanece fuera de alcance;
  • el bump/tag/release se realiza al final del gate.

4. Por qué empezar por un único campo

El teléfono principal se eligió deliberadamente como writer mínimo vertical, no porque fuese el campo más importante del negocio.

La pregunta que se quería responder era:

¿Podemos escribir de Operations a Azure SQL de forma segura, auditable, concurrente, desplegable y comprensible?

El primer writer debía reducir al mínimo las variables de negocio para concentrar el riesgo en la infraestructura de escritura.

El teléfono permitió probar:

identidad
+ autorización
+ API
+ permisos SQL
+ transacción
+ rowversion
+ audit
+ UI
+ CI/CD
+ recuperación

sin mezclar todavía:

  • propagaciones legacy complejas;
  • formularios con múltiples campos;
  • reglas de negocio cruzadas;
  • operaciones masivas;
  • relaciones multisuministro;
  • cambios bancarios;
  • cutover de Access.

Regla aprendida

El primer writer debe ser pequeño en negocio, pero completo en arquitectura.

El valor de esta implementación está en el patrón demostrado, no en la cantidad de columnas editables.


5. Discovery LPD y contrato legacy

Antes de escribir se realizó discovery del formulario Access representativo y del contrato SQL.

LPD — Legacy Product/Process Discovery

Se inspeccionó un Access de producción representativo en copia temporal segura.

Hallazgos para Tfno principal:

  • el formulario Contactos está vinculado a Contactos;
  • Tfno principal es un TextBox bound;
  • el control está habilitado y no bloqueado;
  • no presenta ValidationRule;
  • no presenta InputMask;
  • no tiene eventos específicos de guardado;
  • no se encontró VBA/macros especiales asociados al cambio;
  • Access usa el guardado bound normal;
  • no se identificaron side effects adicionales específicos del teléfono.

Veredicto:

LPD = PASS
confidence = HIGH

Contrato SQL

Campo:

dbo.Contactos.[Tfno principal] nvarchar(255) NULL

Identidad:

dbo.Contactos.Id_Contacto int identity primary key

Control de versión legacy:

dbo.Contactos.SSMA_TimeStamp timestamp / rowversion

No se encontraron:

  • triggers relevantes;
  • checks específicos del teléfono;
  • reglas de normalización.

Hallazgo importante: no todos los campos serán iguales

El email se dejó fuera deliberadamente.

Durante discovery aparecieron indicios de comportamiento/propagación histórica que impiden asumir:

email = otra columna simple

Esto confirma el valor de LPD.

No convertir la tabla Contactos en un CRUD genérico.

Cada capability de escritura debe demostrar antes qué comportamiento legacy está sustituyendo.


6. Arquitectura del writer

Diagrama simplificado:

flowchart TD
    U[Usuario] --> EA[Microsoft Entra / Easy Auth]
    EA --> W[Admin Web DEV]
    W --> N[Nginx]
    N -->|identity headers + internal gate| API[FastAPI]
    API --> AUTH[WriteAuthorizer]
    AUTH --> UC[UpdatePrimaryPhone]
    UC --> ET[ETag / rowversion]
    ET --> SQLW[Dedicated SQL Writer]
    SQLW --> DB[(IXATU_DEV)]
    SQLW --> AUD[(OperationsWriteAudit)]

Vista por responsabilidades:

Browser

Entra / Easy Auth

Nginx internal gate

FastAPI / WriteAuthorizer

Use case

ETag / rowversion

Dedicated SQL writer

Contactos + Audit

Cada capa responde a una pregunta distinta:

Capa Pregunta
Entra ¿Quién eres?
Allowlist / WriteAuthorizer ¿Puedes escribir?
Gate Web→API ¿La identidad llega por el caller esperado?
Use case ¿La operación es válida?
ETag / rowversion ¿Editas la misma versión que leíste?
SQL grants ¿Qué puede modificar técnicamente Operations?
Transacción ¿El cambio queda íntegro?
Audit ¿Quién hizo qué operación y con qué resultado?

7. Capas de seguridad

La seguridad no depende de un único mecanismo.

7.1 Microsoft Entra ID

Entra autentica a la persona.

La identidad relevante para autorización es el Object ID estable, no:

  • email;
  • nombre;
  • alias;
  • UPN mostrado en UI.

7.2 Allowlist de writers

Variable GitHub:

IXATU_DEV_WRITE_ALLOWED_PRINCIPAL_IDS

Se inyecta en la API como:

IXATU_WRITE_ALLOWED_PRINCIPAL_IDS

Contiene Object IDs separados por comas.

Ejemplo conceptual:

uuid-jesus,uuid-xabier,uuid-izaskun,...

A fecha del documento, se ha habilitado el piloto para los seis miembros acordados del equipo:

  • Xabier;
  • Izaskun;
  • Jesús;
  • Elena;
  • Odei;
  • Chechu / Joseramón.

7.3 Fail closed

La autorización está diseñada para fallar cerrada.

Un Object ID no autorizado produce:

HTTP 403

Además, el parser actual de allowlist tiene una característica operativa relevante:

Si la variable contiene un UUID mal formado, la construcción de la allowlist falla y el resultado efectivo es una lista vacía.

Consecuencia:

UUID inválido en configuración
→ nadie puede escribir
→ fallo seguro

Esto obliga a cuidar la actualización de la variable.

7.4 Gate interno Web → API

La API no confía simplemente en headers enviados por el navegador.

Nginx:

  • elimina headers de identidad inbound no confiables;
  • inyecta server-side la identidad proporcionada por Easy Auth;
  • añade un secreto interno X-Ixatu-Write-Gate;
  • no devuelve ese secreto al browser;
  • desactiva forwarding indiscriminado de headers en la ruta sensible.

La API exige:

gate válido
+ provider = aad
+ Object ID válido
+ Object ID allowlisted

Esto reduce el riesgo de que otro caller dentro del Container Apps Environment fabrique headers y escriba directamente.

7.5 API interna

La Container App de FastAPI usa ingress interno.

La Web es el ingress externo y el proxy autorizado.

7.6 SQL privilege minimization

La API writer no usa:

db_owner
db_datawriter
db_datareader

El principal writer recibe únicamente los permisos necesarios para su capability.


8. Autenticación, autorización y permisos SQL no son lo mismo

Esta distinción es esencial.

flowchart TD
    A[Cuenta Entra] --> B{Autenticado?}
    B -->|No| X[No hay identidad writer]
    B -->|Sí| C{Object ID allowlisted?}
    C -->|No| D[403]
    C -->|Sí| E[Operations acepta la operación]
    E --> F[Principal SQL técnico]
    F --> G{¿Tiene grant exacto?}
    G -->|No| H[SQL rechaza]
    G -->|Sí| I[Write]

Tener cuenta Entra no equivale a poder escribir

Entra responde:

Esta persona es X.

Operations responde después:

X está o no autorizada para esta capability.

Tener usuario SQL personal no es requisito

Los usuarios humanos no escriben desde Operations usando:

xabier_dev
izaskun_dev
jesus_dev
...

Operations utiliza su principal técnico.

Usuarios SQL humanos existentes

En IXATU_DEV existen actualmente, entre otros:

chechu_dev
izaskun_dev
jesus_dev
odei_dev
xabier_dev

con:

db_datareader
db_datawriter

También existe:

i3code → db_owner

Estos accesos son independientes de Operations y son considerablemente más amplios.

Hallazgo de gobierno:

Los usuarios SQL personales pueden ser útiles para SSMS/administración DEV, pero no deben convertirse en el mecanismo de autorización de la aplicación.

A futuro conviene auditar su necesidad y privilegios.


9. Alta de usuarios autorizados en DEV

Paso 1 — obtener Object ID Entra

Ejemplo:

az ad user show \
  --id [email protected] \
  --query id \
  -o tsv

Para el propio usuario:

az ad signed-in-user show --query id -o tsv

Paso 2 — actualizar allowlist

gh variable set IXATU_DEV_WRITE_ALLOWED_PRINCIPAL_IDS \
  --repo IXATU/ixatu-operations \
  --body '<uuid-1>,<uuid-2>,<uuid-3>'

Los Object IDs no son contraseñas, pero no deben difundirse innecesariamente.

Paso 3 — desplegar main

gh workflow run deploy-dev.yml \
  --repo IXATU/ixatu-operations \
  --ref main

El workflow actualiza la configuración efectiva de la API.

Paso 4 — prueba sin escritura

Para comprobar autorización sin mutar datos:

  1. entrar en DEV;
  2. autenticar mediante Entra;
  3. abrir el contacto de prueba;
  4. pulsar Editar;
  5. verificar que aparece el input;
  6. pulsar Cancelar.

Si el usuario no está autorizado:

No tienes permiso para editar este campo.

No añadir a la allowlist

No deben añadirse:

ixatu_operations_dev_ro
ixatu_operations_dev_phone_writer

porque son usuarios SQL, no identidades Entra humanas.

Tampoco deben añadirse «Claude», «ChatGPT», «Cursor» o agentes equivalentes como si fueran personas.

Si en el futuro un agente necesita escritura, tendrá una identidad técnica propia.


10. Reader y writer SQL

La separación de credenciales es un invariante.

Reader

Variables:

IXATU_SQL_*

Principal:

ixatu_operations_dev_ro

Protección observada:

db_denydatawriter

Además, el adapter de lectura usa:

ApplicationIntent=ReadOnly

ApplicationIntent=ReadOnly es una intención del cliente, no una barrera de permisos. La defensa real es SQL privilege minimization.

Writer

Variables:

IXATU_WRITE_SQL_*

Principal:

ixatu_operations_dev_phone_writer

Permisos efectivos para el primer writer:

SELECT:
- Id_Contacto
- Tfno principal
- SSMA_TimeStamp

UPDATE:
- Tfno principal

INSERT:
- dbo.OperationsWriteAudit

No tiene:

db_owner
db_datawriter
db_datareader
UPDATE general de Contactos
INSERT en Contactos
DELETE
ALTER
CONTROL

Principio

Un writer no recibe permisos para la tabla; recibe permisos para la capability.


11. Concurrencia: rowversion, ETag e If-Match

El legacy dispone de:

SSMA_TimeStamp

que SQL Server trata como rowversion.

Operations lo expone como un ETag opaco.

Lectura fresca antes de editar

GET /customers/{customer_id}/contact/primary-phone

Respuesta conceptual:

primaryPhone = valor
ETag = "version-A"

La UI no usa el teléfono previamente cargado en la ficha como snapshot de concurrencia.

Al pulsar Editar hace un GET específico fresco.

Guardado

PATCH /customers/{customer_id}/contact/primary-phone
If-Match: "version-A"

SQL

Patrón conceptual:

UPDATE dbo.Contactos
SET [Tfno principal] = ?
OUTPUT inserted.[Tfno principal], inserted.SSMA_TimeStamp
WHERE Id_Contacto = ?
  AND SSMA_TimeStamp = ?;

Resultado

Si la fila sigue en la versión A:

1 row
→ 200
→ nuevo ETag B

Si alguien la cambió:

0 rows
→ existe el contacto pero rowversion cambió
→ 412 Precondition Failed

Esto evita el lost update.

Regla

Nunca sobrescribir silenciosamente un cambio concurrente.

La UI ante 412:

  • conserva el borrador;
  • informa del conflicto;
  • invalida el ETag;
  • obliga a Recargar valor;
  • no reintenta automáticamente.

12. Transacción y auditoría

Transacción

El writer usa:

autocommit = False

y commit/rollback explícitos.

La infraestructura SQL inicial se corrigió para utilizar:

SET XACT_ABORT ON
BEGIN TRY
BEGIN TRANSACTION
...
COMMIT
BEGIN CATCH
ROLLBACK
THROW

Audit

Tabla:

dbo.OperationsWriteAudit

Registra metadatos operativos, no datos de negocio sensibles.

Incluye conceptualmente:

  • timestamp UTC;
  • actor Object ID;
  • operación;
  • contact_id;
  • request/correlation ID;
  • resultado.

Operación:

contact.primary_phone.update

No PII

No deben almacenarse en audit/logs:

  • teléfono anterior;
  • teléfono nuevo;
  • nombre;
  • NIF;
  • email;
  • dirección;
  • CUPS;
  • IBAN.

Evidencia real

Primera prueba backend:

success
conflict
success

correspondiente a:

write
stale write rechazado
restore

Prueba humana UI:

success
success

correspondiente a:

primera escritura UI
restauración del valor original

13. Semántica del teléfono y Sin informar

El legacy acepta:

NULL
o
nvarchar <= 255

No se ha introducido:

  • regex española;
  • normalización de +34;
  • eliminación automática de espacios;
  • formato telefónico;
  • truncado silencioso.

La UI conserva el valor literal.

Sin informar

Distingue:

""   → cadena vacía
NULL → valor no informado

La opción actual Sin informar envía:

{"primaryPhone": null}

Esto refleja la semántica SQL real.

Posible mejora UX a observar durante estabilización:

  • Sin teléfono
  • Borrar teléfono

No debe cambiarse la semántica sin decidirlo explícitamente.


14. Entornos: localhost, DEV y PROD

Localhost

El localhost no es por sí mismo un entorno de datos.

Depende de la configuración.

La aceptación local usada en Slice B fue sintética, con Playwright interceptando respuestas como:

SYN-OLD
SYN-FRESH
SYN-WRITER

No hubo escritura SQL.

El docker-compose.yml local actual no inyecta por defecto:

IXATU_WRITE_SQL_*
IXATU_WRITE_ALLOWED_PRINCIPAL_IDS
IXATU_WRITE_INTERNAL_GATE

Por tanto:

Localhost sirve para validar UI, contrato HTTP y estados de error con fixtures, no para demostrar el writer real.

DEV

URL de referencia:

la URL pública de la aplicación en Azure Container Apps

Base:

ixatu-dev.database.windows.net
IXATU_DEV

DEV es el laboratorio de escritura.

Aquí se validan:

  • Easy Auth;
  • Object ID;
  • gate;
  • writer SQL;
  • SQL real;
  • audit;
  • concurrencia;
  • comportamiento humano.

PROD

URL de referencia:

la URL pública de la aplicación en Azure Container Apps

Base:

ixatu.database.windows.net
IXATU

PROD dispone del pilot de lectura protegido, pero no se ha configurado el writer v0.9.0.

No existen todavía como capability promovida de escritura PROD:

  • writer SQL de teléfono;
  • allowlist de writers PROD;
  • grants writer;
  • audit writer asociado a esta capability;
  • deployment de write habilitado.

DEV y PROD son entornos separados. Nunca se «copia» informalmente la configuración writer de DEV a PROD.


15. Flujo de implementación, merge, Bake y Deploy DEV

Flujo habitual de feature

flowchart TD
    A[main estable] --> B[feature branch desde SHA exacto]
    B --> C[TDD / implementación]
    C --> D[Tests locales]
    D --> E[Playwright sintético]
    E --> F[Docker Buildx Bake]
    F --> G[Draft PR]
    G --> H[PR CI]
    H --> I[Auditoría Director]
    I --> J[Ready]
    J --> K[Squash merge main]
    K --> L[Push main]
    L --> M[Deploy DEV automático]
    M --> N[Human Acceptance DEV]
    N --> O[Issue closure / Release Gate]

CI del PR

Los checks actuales incluyen:

Python
  → Ruff
  → pytest
  → wheel

Admin Web
  → npm ci
  → typecheck
  → build
  → Playwright synthetic browser tests

Docker
  → docker buildx bake

Deploy DEV

flowchart TD
    A[push main] --> B[GitHub Actions deploy-dev.yml]
    B --> C[OIDC login Azure]
    C --> D[docker buildx bake --push]
    D --> E[ACR DEV<br/>tag = commit SHA]
    E --> F[Update API Container App]
    E --> G[Update Web Container App]
    F --> H[Stable revision]
    G --> H
    H --> I[Public Web smoke]
    I --> J[/api/health]
    J --> K[Swagger/OpenAPI DEV checks]

Características:

  • autenticación GitHub→Azure mediante OIDC;
  • no se almacena client secret de deployment;
  • imágenes etiquetadas por commit SHA;
  • ACR DEV;
  • actualización idempotente de Container Apps;
  • secrets actualizados antes de nueva revisión;
  • min replicas = 0, max replicas = 1;
  • modo de revisión single;
  • smoke a través de la Web pública.

Bake

docker buildx bake es parte del gate de reproducibilidad.

No sólo se comprueba:

"el TypeScript compila"

sino:

"las imágenes reales de API y Web se pueden construir"

Feature branch deployment

No es el camino normal.

Durante Slice A se usó temporalmente una credencial OIDC exacta de feature branch porque era necesario validar infraestructura writer antes del merge.

La credencial:

github-feature-75-write-foundation

se eliminó antes de integrar.

Slice B volvió al patrón normal:

feature
→ CI/local
→ merge
→ deploy DEV desde main

Lección

No abrir confianza OIDC de feature branches salvo que exista un motivo operativo explícito y temporal.


16. Evidencias y pruebas realizadas

Backend

  • 455 pytest PASS.
  • Ruff PASS.
  • Docker Bake PASS.
  • security tests PASS.
  • writer SQL tests PASS.
  • API contract tests PASS.

UI

  • 85 Playwright PASS tras Slice B.
  • desktop 1440 px PASS.
  • mobile 390 px PASS.
  • no overflow relevante.
  • typecheck PASS.
  • Vite build PASS.

Write E2E backend real

Fixture DEV:

un contacto de prueba de DEV

Se comprobó además que no existía en PROD como fixture equivalente usado para la prueba.

Secuencia:

GET snapshot
→ PASS

PATCH synthetic
→ 200

PATCH con ETag antiguo
→ 412

restore exacto
→ 200

GET final
→ original restaurado

Audit:

success
conflict
success

Human Acceptance UI

Secuencia:

Editar
→ Cancelar
→ PASS / no write

Editar
→ Guardar valor sintético
→ PASS

SQL verification
→ ui_write_confirmed = 1

Editar
→ restaurar original
→ PASS

SQL verification
→ ui_write_confirmed = 0

Audit adicional:

success  ← UI write
success  ← restore

Resultado:

Primer writer humano de Operations operativo end-to-end en IXATU_DEV.


17. Hallazgos y decisiones significativas

17.1 El writer no necesita usuarios SQL humanos

Operations no autentica en SQL como:

jesus_dev
xabier_dev
izaskun_dev

Los usuarios trabajan con Entra.

La aplicación utiliza un principal técnico.

17.2 Los usuarios SQL personales son otro canal

Los usuarios _dev existentes con db_datareader + db_datawriter son útiles potencialmente para SSMS o administración, pero no son parte del modelo de seguridad de la aplicación.

17.3 Reader y writer deben permanecer separados

Nunca ampliar el reader existente para «aprovecharlo» y escribir.

17.4 Permiso por capability, no por tabla

El writer sólo puede modificar la columna necesaria.

17.5 La UI no decide permisos

No se implementa lógica como:

si email == xabier@...
  mostrar write

La API es la autoridad.

17.6 Un GET de ficha no es suficiente para editar

El snapshot de edición debe ser fresco y específico de la capability.

17.7 412 es éxito del sistema de protección

Un 412 Precondition Failed en stale write significa:

la protección de concurrencia ha funcionado

no un fallo del writer.

17.8 Audit no debe convertirse en una copia de los datos

Audit registra operación, actor y resultado; no los valores sensibles.

17.9 El primer writer fue costoso a propósito

Se pagó el coste de construir el carril reusable:

  • auth;
  • authz;
  • gate;
  • writer credentials;
  • grants;
  • ETag;
  • audit;
  • deployment;
  • tests.

Los siguientes writers deben reutilizar esta infraestructura.

17.10 Versionar no es lo mismo que terminar de programar

Aunque la capability ya funciona, v0.9.0 permanece en Release Gate.

El bump se realiza después de estabilización del equipo.


18. Patrón reusable para futuros writers

Separación recomendada:

Rail reusable

Microsoft Entra
Object ID
WriteAuthorizer
internal Web→API gate
reader/writer credential split
SQL least privilege
ETag / If-Match
transaction conventions
audit
sanitized errors
GitHub Actions wiring
DEV-first human gates

Capability específica

Para teléfono:

route primary-phone
DTO primaryPhone
validation null|string<=255
UPDATE Tfno principal
column grant
PrimaryPhoneField UI
tests de teléfono

Fórmula conceptual

NUEVO WRITER
=
LPD
+ capability específica
+ use case
+ SQL mínimo
+ permisos mínimos
+ concurrencia
+ audit
+ tests
+ aceptación DEV

Diagrama:

flowchart LR
    LPD[LPD] --> DOM[Use case]
    DOM --> API[HTTP capability]
    API --> AUTH[Rail de seguridad existente]
    AUTH --> SQL[SQL exacto]
    SQL --> GRANT[Grant mínimo]
    GRANT --> TEST[Test]
    TEST --> DEV[Human Acceptance DEV]

19. Qué no debemos generalizar todavía

No construir aún:

GenericRepository.update(table, fields)
GenericCRUD<Contactos>
GenericWriter

Tampoco convertir todo Contactos en editable automáticamente porque exista un primer writer.

Secuencia recomendada:

writer 1
→ aprendemos

writer 2
→ identificamos repetición real

writer 3
→ abstraemos sólo lo demostrado

No abstraer la incertidumbre.


20. Roadmap global recorrido

El siguiente mapa es funcional y arquitectónico. Para el ledger exacto de tags/releases debe consultarse GitHub Releases.

flowchart TD
    V01[v0.1.x<br/>Walking Skeleton + Azure DEV + CI/CD] --> V02[Infra / pilot protegido / delivery]
    V02 --> V03[Customer workspace / discovery UI]
    V03 --> V04[Cliente + Suministros<br/>lectura real]
    V04 --> V05[Contactos<br/>lectura operativa]
    V05 --> V06[Ofertas + Contrato Comercial]
    V06 --> V07[Home / Informes / Vencimientos]
    V07 --> V08[Gestiones + contextos frecuentes]
    V08 --> V09[v0.9.0<br/>Primer writer controlado]
    V09 --> NOW[Release Gate DEV<br/>ESTAMOS AQUÍ]
    NOW --> F2[Formulario Contacto]
    F2 --> F3[Patrón reusable writers]
    F3 --> F4[CLI / agentes]
    F4 --> F5[Operaciones multisuministro]
    F5 --> F6[Pilot writer PROD]

Lectura resumida

Etapa 1 — plataforma base

Objetivos ya recorridos:

  • repositorio y arquitectura;
  • FastAPI;
  • React/Vite;
  • Docker;
  • Buildx/Bake;
  • Azure Container Apps;
  • ACR;
  • GitHub Actions;
  • OIDC;
  • separación DEV/PROD;
  • smokes;
  • release gates.

Etapa 2 — sustitución progresiva de consultas Access

Objetivos recorridos:

  • Home;
  • clientes;
  • suministros;
  • contactos;
  • contratos;
  • ofertas;
  • vencimientos;
  • informes;
  • gestiones.

Etapa 3 — primera escritura

Objetivo actual:

Operations read-only
→ Operations read/write controlado en DEV

Etapa 4 — formularios

Próxima frontera:

1 capability de 1 campo
→ formulario coherente

Etapa 5 — operaciones de negocio compuestas

Después:

1 entidad
→ varias entidades
→ operaciones multisuministro

Etapa 6 — cutover progresivo

Mucho más adelante:

writer estabilizado DEV
→ pilot PROD
→ convivencia
→ desactivar comportamiento Access equivalente
→ estabilizar
→ migración/modelado posterior

21. Seis frentes aprobados desde este punto

Los seis frentes quedan aprobados como marco de dirección.

Frente 1 — cerrar bien v0.9.0

Estado:

ACTUAL

Objetivo:

  • estabilización de equipo en DEV;
  • validar autorización de usuarios;
  • recoger feedback;
  • fixes mínimos;
  • bump 0.9.0;
  • CI;
  • Deploy DEV final;
  • tag + GitHub Release.

No añadir nuevas capabilities durante este gate.


Frente 2 — formulario Contacto

Estado:

SIGUIENTE

Objetivo:

  • discovery LPD de campos del formulario;
  • identificar validaciones, relaciones y side effects;
  • definir bounded form;
  • guardar varios campos coherentes;
  • una estrategia de concurrencia;
  • una transacción consistente;
  • audit de operación.

No asumir que todos los campos son tan simples como teléfono.


Frente 3 — consolidar patrón reusable de writers

Objetivo:

separar definitivamente:

infra reusable
vs
lógica específica

Sin construir un framework genérico prematuro.


Frente 4 — CLI / agentes IA

Objetivo:

permitir que automatizaciones autorizadas utilicen las mismas capabilities que la UI.

Regla:

agente
→ API
→ misma auth/authz
→ mismo ETag
→ mismo audit

No:

agente
→ SQL directo

Frente 5 — operaciones multisuministro

Objetivo:

operaciones de alto valor como:

  • cambios coordinados;
  • acciones sobre varios suministros;
  • posibles cambios bancarios;
  • operaciones masivas con previsualización;
  • audit de operación.

Debe diseñarse como operación de negocio, no como un bucle de UPDATE.


Frente 6 — primer writer PROD

Objetivo futuro.

Requiere gate separado:

  • principal writer PROD;
  • grants mínimos;
  • allowlist PROD;
  • audit;
  • backup/recovery;
  • deployment;
  • acceptance;
  • convivencia con Access;
  • rollback;
  • seguimiento.

No copiar DEV automáticamente.


22. Frente 2 — próximo objetivo: formulario Contacto

La siguiente pregunta no es:

«¿Qué campo editamos ahora?»

La pregunta correcta es:

¿Cuál es el primer conjunto coherente de datos de Contacto que debe guardarse como una operación única?

Discovery previo

flowchart TD
    A[Formulario Access Contactos] --> B[Controls / bindings]
    B --> C[Validation rules]
    C --> D[VBA / macros / queries]
    D --> E[Side effects]
    E --> F[SQL schema]
    F --> G[Relations]
    G --> H[Risk classification]
    H --> I[Form contract Operations]

Clasificación esperada

Ejemplo conceptual:

Campo Estado
Teléfono principal Conocido / bajo riesgo
Teléfono secundario Discovery necesario
Nombre / razón Discovery necesario
Apellidos Discovery necesario
Email Atención: propagación histórica detectada
Dirección Discovery necesario
Otros flags Discovery necesario

Evolución técnica

Hoy:

PATCH primary-phone
→ 1 field
→ 1 ETag

Posible siguiente modelo:

PATCH /customers/{id}/contact
→ ContactEditCommand
→ varios campos permitidos
→ 1 snapshot/version
→ 1 transacción
→ 1 audit coherente

No se decide todavía el contrato final hasta completar LPD.


23. Evolución hacia operaciones multisuministro

Las operaciones masivas serán una fase posterior.

Ejemplo de necesidad:

Cambiar cuenta bancaria
para varios suministros

No debe modelarse como:

for supply in supplies:
    update(supply)

sin contrato de negocio.

Preguntas obligatorias:

  • ¿qué entidad posee realmente la cuenta?;
  • ¿se almacena en Contacto, Suministro o relación?;
  • ¿qué side effects tiene Access?;
  • ¿varios suministros deben compartir la misma referencia?;
  • ¿cómo se valida previamente?;
  • ¿cómo se autoriza?;
  • ¿qué ocurre ante éxito parcial?;
  • ¿cómo se reintenta?;
  • ¿cómo se audita sin almacenar IBAN?;
  • ¿cómo se revierte?;
  • ¿qué confirmación humana se exige?

UX conceptual:

Operación: cambiar cuenta

seleccionar suministros

previsualizar alcance

validar

confirmación

ejecutar operación

resultado por lote

audit

Aquí Operations dejará de ser simplemente un reemplazo de Access y podrá ofrecer una ergonomía operativa superior.


24. CLI y agentes IA

La CLI actual ixatu puede evolucionar, pero no debe convertirse en un atajo a SQL.

Modelo deseado:

flowchart LR
    H[Humano React] --> API[Operations API]
    A[Agent / CLI] --> API
    API --> AUTH[Auth + authz]
    AUTH --> UC[Use cases]
    UC --> SQL[Writer]
    SQL --> DB[(Azure SQL)]

Identidades:

Jesús
→ Entra Object ID humano

Izaskun
→ Entra Object ID humano

Automation Agent DEV
→ identidad técnica propia

Ventajas:

  • mismo contrato;
  • mismas validaciones;
  • mismo ETag;
  • mismo audit;
  • atribución clara;
  • sin credenciales SQL entregadas a agentes.

Posible línea futura:

ixatu contact ...

pero debe diseñarse como cliente de API.


25. Paso futuro a PROD

El paso a producción no es «activar una variable».

Debe ser una iniciativa explícita.

Gate futuro

DEV estable
→ patrón comprendido
→ acceptance suficiente
→ diseño PROD
→ principal writer PROD
→ grants mínimos
→ allowlist PROD
→ deployment controlado
→ smoke
→ human acceptance
→ pilot
→ observación

Convivencia

Durante el pilot:

Access sigue activo
+
Operations writer acotado

Cuando una capability esté suficientemente estabilizada:

desactivar función equivalente en Access
→ observar
→ estabilizar
→ continuar

Después, y no antes, puede plantearse el modelado/migración a PostgreSQL.


26. Reglas operativas y mnemotécnicas

Primero sustituir el comportamiento de Access; después sustituir su modelo de datos.

Un writer no recibe permisos para la tabla; recibe permisos para la capability.

Autenticación dice quién eres; autorización dice qué puedes hacer.

Entra no sustituye la allowlist.

ApplicationIntent=ReadOnly no sustituye los permisos SQL.

El ETag pertenece al snapshot de edición, no a la pantalla que lleva minutos abierta.

HTTP 412 no es un fallo: es una escritura peligrosa correctamente rechazada.

Audit registra la operación, no copia la PII.

DEV es donde aprendemos; PROD no es nuestro laboratorio.

Feature → CI → Director → merge → Deploy DEV → Human Acceptance.

No abstraer la incertidumbre.

El primer writer demuestra el carril; el siguiente demuestra que el carril es reusable.

Una operación multisuministro es una operación de negocio, no un bucle de UPDATE.


27. Riesgos y deuda conocida

27.1 Usuarios SQL personales amplios en DEV

Existen usuarios humanos con:

db_datareader
db_datawriter

e incluso un usuario técnico/externo con:

db_owner

No bloquea v0.9.0, pero merece auditoría posterior.

27.2 App Registration DEV

Durante Slice A se encontró una App Registration IXATU Operations DEV ya existente.

Su procedencia no quedó demostrada.

No bloqueó el writer, pero el inventario de identidades debería mantener trazabilidad de creación/ownership.

27.3 NPM advisory heredado

Durante Slice B, npm audit reportó una vulnerabilidad heredada en nanoid.

No fue introducida por el slice y las dependencias estaban congeladas.

Debe tratarse como deuda separada.

27.4 Release metadata todavía en 0.8.2

A fecha de este documento, el código funcional de writer está integrado, pero el bump de versión formal está diferido hasta cerrar el Release Gate.

Esto es intencionado.

27.5 PROD writer inexistente

No confundir:

PROD Web autenticada

con:

PROD write habilitado

Son cosas distintas.


28. Evidencia operativa (sin identificadores privados)

El Release Gate de v0.9.0 dejó evidencia de:

  • slices A/B de foundation + UI;
  • Deploy DEV sucesivos del backend writer, UI writer y ampliación de allowlist;
  • escritura real en Azure SQL DEV;
  • rechazo 412 de stale write;
  • restauración exacta del valor de prueba;
  • audit de éxito / conflicto / éxito;
  • aceptación humana de UI.

Los Object IDs, IDs de contacto de prueba, URLs concretas de ACA y SHAs internos pertenecen al ledger operativo del repositorio IXATU Operations y no se publican aquí.


Conclusión

v0.9.0 representa un cambio de fase para IXATU Operations.

Hasta aquí, el proyecto había demostrado que podía consultar y presentar el legacy de manera moderna. El primer writer demuestra ahora que puede modificarlo sin renunciar a control, trazabilidad y seguridad.

La secuencia estratégica queda:

lectura legacy moderna

primer writer controlado

estabilización DEV

formulario Contacto

patrón reusable de writers

automatización / CLI

operaciones multisuministro

pilot writer PROD

sustitución progresiva de Access

El siguiente frente aprobado es Frente 2 — formulario Contacto, comenzando por LPD y diseño, no por programación inmediata.

La prioridad sigue siendo la misma:

hacer cada paso pequeño, observable, reversible y comprensible antes de aumentar el radio de escritura.