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 oopenapi-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). Ocheca-rotas.pysegue como precedente do gate de contrato.
Risco declarado (constituição §1.2): a capacidade real do
openapi-generatorde gerar@ClientMicronaut 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) comcodede 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). Oint-sascarmanté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 doxadm-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
uploadDatado 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.
- Sempre 200 + erro no corpo + Sentry onde PowerSync ou fila exige: o write-back
-
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.