Pular para conteúdo

0020 — Contrato app↔app

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-08-04 · Decidido em: 2026-08-04

Contexto

A comunicação server-server da casa roda, mas não é segura para evoluir. Cada par de apps redefine os dois lados do fio à mão: zero tipo compartilhado, então um rename de campo quebra em silêncio (compila dos dois lados, falha no wire); sem versionamento de contrato, publicar uma mudança arrisca quebrar quem já consome; sem teste de contrato, ninguém sabe que o produtor e o consumidor divergiram até produção reclamar.

A decisão 0012 já fez metade do caminho: o micronaut-openapi deriva o OpenAPI dos @Controller ao compilar, e o checa-rotas.py confere a prosa contra esse spec no gate. Mas 0012 é explícita sobre o próprio limite — cobre rota e método; o corpo (request/response) fica para revisão de PR. É essa lacuna do corpo que morde quando um app chama outro: a rota confere, o shape do JSON não é contratado por ninguém.

O padrão ARQUIVO-FATO do integrador-server/docs/public/contratos/ (0014/0016) já publica contrato de um repo para o outro consumir — falta elevá-lo a norma de plataforma para todo par app↔app, com um mecanismo de tipo e versão.

Decisão

Esta decisão estende a 0012 para o corpo do contrato: quem recebe o chamado dita o contrato. 0012 continua valendo para rota/método (prosa × OpenAPI no gate); aqui fecha o request/response.

  • O receptor publica o OpenAPI; o consumidor gera o cliente dele. O app que expõe o endpoint gera e publica seu OpenAPI via micronaut-openapi (como já faz por 0012). O app que chama gera @Client + DTOs desse OpenAPI, no build, com o openapi-generator (gerador de cliente Micronaut do lado consumidor) — sem lib de contrato escrita à mão dos dois lados. Não confunda os dois geradores: micronaut-openapi = servidor (spec sai do controller); openapi-generator = consumidor (cliente sai do spec). O checa-rotas.py segue como precedente do gate de contrato.

Risco declarado (constituição §1.2): a capacidade real do openapi-generator de gerar @Client Micronaut idiomático é confirmada na fonte na Fase 3, antes de qualquer app depender disso. Aqui a norma; a prova vem na fase que a implementa. A coordenada/versão do gerador é Open Question da spec da fase.

  • Versionamento com compatibilidade. Publicar uma versão nova de um contrato não pode quebrar um caller pinado na anterior — a versão anterior continua servível durante a janela de deprecação, e o caller migra quando puder. Mudança incompatível = versão nova (path versionado /api/v2/... ou versão do artefato de contrato), não edição in-place da que está no ar. (O número concreto de janela de deprecação é decisão da spec da Fase 3.)

  • Envelope de auth M2M. Chamada máquina-a-máquina entre apps carrega Bearer, com a env nomeada pelo destino<APP>_URL + <APP>_API_TOKEN, um token por callee, mesmo valor no validador e em todo caller (a convenção de nome é o piso, Segurança §Segredos). Erro de auth responde RFC-7807 (application/problem+json) com code de máquina estável (401/403). O token rota por rotação; é 1 token por instância (não compartilhado entre clientes — há uma instância por cliente). O int-sascar mantém token-como-tenant (o token identifica o cliente na entrada) — exceção existente, não se força a mudar.

  • Dois eixos de auth, não confundir. O contrato cobre só o M2M (envelope acima). A user-auth (Firebase / xadm_session / central, para quem tem usuário de verdade) é problema do xadm-seguranca, resolvido por lib, fora deste contrato. Um endpoint app↔app é M2M; uma tela com usuário é user-auth.

  • Status HTTP por caso — não há um número universal.

    • Sempre 200 + erro no corpo + Sentry onde PowerSync ou fila exige: o write-back uploadData do PowerSync e o push legado do X-Adm travam a fila do cliente se recebem não-2xx para erro de negócio (PowerSync §Config). Aí o 200-sempre é obrigatório e não muda — o erro vai no corpo, o Sentry registra.
    • Par app↔app novo, SEM PowerSync no meio, usa status HTTP real (4xx/5xx). Não se propaga o 200-sempre para contrato novo só por hábito: onde não há fila que trave, o status real é o contrato honesto.
  • request-id propagado entre hops. Um header de correlação (request-id) atravessa o envelope M2M em todo hop (integrador ↔ sascar ↔ plataforma), populando o MDC do log de cada app. O filtro que gera/propaga vive no xadm-comum-web (0019, Fase 2). Requisição rastreável ponta-a-ponta no multi-hop — hoje não existe em nenhum app (gap declarado em Java/Micronaut), vira norma aqui.

  • Contract test consumidor × spec. O gate de contrato binda os dois lados: um teste no consumidor casa o @Client/DTOs gerados contra o OpenAPI do receptor, e um teste prova que a versão N-1 continua servível depois de publicar a N. É o que fecha "o corpo diverge em silêncio" que 0012 deixou aberto.

Alternativas descartadas

  • Deixar como 0012 (só rota/método) e cobrir o corpo por revisão de PR. É o estado atual — e o estado atual é o problema: rename de campo quebra no wire sem gate. Revisão humana não é oráculo de shape.
  • Lib de contrato escrita à mão (DTOs compartilhados num módulo). Acopla os dois lados no mesmo artefato e no mesmo ciclo de release; o receptor deixa de ser dono do próprio contrato. O OpenAPI-do-receptor mantém o dono correto (quem recebe dita) e o consumidor gera — sem código de contrato mantido a quatro mãos.
  • Forçar status HTTP real em todo endpoint. Quebraria o write-back do PowerSync e o push legado (fila trava). O status-por-caso preserva o 200-sempre onde a fila exige e adota o status real só onde é seguro.
  • Versionar in-place (editar o contrato no ar). Quebra caller pinado. Compat com janela de deprecação é o custo aceito para não quebrar ninguém.

Consequências

  • Todo par app↔app novo casa num contrato tipado e versionado, com teste bindando produtor↔consumidor. Rename de campo passa a falhar no gate, não no wire.
  • O receptor é dono do contrato. Muda o contrato criando versão nova; o consumidor migra na janela. Ninguém edita o shape do outro.
  • Generaliza o ARQUIVO-FATO para a plataforma — o docs/public/contratos/ de um repo deixa de ser padrão de um app e vira o mecanismo de publicação de contrato de qualquer receptor.
  • Depende de duas confirmações de fase (§1.2): capacidade do openapi-generator (Fase 3) e o número da janela de deprecação (spec Fase 3). A norma não pressupõe; a fase prova.
  • request-id e o filtro de correlação nascem no xadm-comum-web — este contrato é um dos motivos de a Fase 2 existir.