API URL do frontend resolvida em runtime (k8s)

TLDR: a API base URL é servida ao browser por /api/env, lida do pod a cada requisição — nunca inlinada no bundle nem gravada no HTML. A imagem Docker fica agnóstica de ambiente (build once, promove para staging/production) e as páginas estáticas continuam funcionando.

Por que runtime

O pipeline k8s da WeHive builda a imagem a partir da tag e a entrega para os dois ambientes com o mesmo tag no ECR — staging e production rodam o mesmo trgclub-frontend:<tag>. Uma URL definida em build time faria production chamar a API de staging.

Fluxo

  1. O secret fornece API_URL ao pod, por ambiente, sem barra final.
  2. pages/api/env responde, a cada requisição, window.__ENV = { ... } como application/javascript com Cache-Control: no-store.
  3. pages/_document.tsx carrega essa rota com um <script src="/api/env"> síncrono no <Head>.
  4. Env (src/infra/env) lê process.env no server e window.__ENV no client.
  5. apiUrl(path) (src/services/fetch/apiUrl) monta toda URL de API.

mermaid flowchart TD A["secret → pod<br/>API_URL"] --> C["pages/api/env<br/>allowlist, por requisição<br/>no-store"] D["_document &lt;Head&gt;<br/>&lt;script src='/api/env'&gt;"] -->|"bloqueia o parse"| C C --> E["window.__ENV"] E --> F["chunks do Next (defer)"] F --> G["Env.get / apiUrl"]

Regras que não podem ser quebradas

  • O script precisa continuar síncrono. Ele carrega uma exceção local a @next/next/no-sync-scripts justamente por isso: async/defer fariam os chunks do Next rodarem antes do env existir. O teste src/infra/env/runtimeEnvScript.test.ts falha se alguém adicionar um dos dois, mover a tag para depois do primeiro chunk, ou remover a tag.
  • Nada de gravar env no HTML em tempo de render. O app tem 15 páginas estaticamente otimizadas cujo HTML é gerado no build; qualquer valor escrito ali fica congelado com o valor de build (vazio). Foi assim que a tentativa anterior falhou.
  • Só o que está na allowlist vai ao browser. PUBLIC_KEYS em pages/api/env.ts tem hoje uma única entrada, API_URL. Não existe varredura por prefixo.
  • A base canônica é sem barra final. Sempre usar apiUrl(path) / apiBaseUrl(), nunca concatenar à mão — a mistura de ${API_URL}path e ${API_URL}/path já quebrou phone auth e cadastro inicial em staging.
  • Falha é ruidosa. Sem API_URL no pod, /api/env responde 500 com um throw; no browser, Env.getOrThrow lança nomeando o script que não carregou. Nunca voltar a devolver undefined em silêncio — o axios cairia na origem do frontend e produziria 404.

Custo

Uma requisição same-origin bloqueante por page load completo. O handler responde em ~2 ms; o custo real para o usuário é um round trip a mais na conexão já aberta. Navegação client-side não paga nada.

Contratos

  • Env.get(key): string | undefined — não lança.
  • Env.getOrThrow(key): string — lança, e diz se a causa foi o script não ter carregado.
  • apiUrl(path): string — base obrigatória; lança se faltar.
  • optionalApiUrl(path): string — devolve "" se faltar; usado só em caminho cosmético (avatar), onde derrubar a tela seria pior que a imagem padrão.
  • Ingress: o ingress do frontend carrega nginx.ingress.kubernetes.io/proxy-buffer-size: 16k (+ proxy-buffers-number: 4) porque o cookie de sessão do next-auth (~3,7 KB) excede o buffer padrão do nginx; sem isso, a navegação pós-login retorna 502.

Referências