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
- O secret fornece
API_URLao pod, por ambiente, sem barra final. pages/api/envresponde, a cada requisição,window.__ENV = { ... }comoapplication/javascriptcomCache-Control: no-store.pages/_document.tsxcarrega essa rota com um<script src="/api/env">síncrono no<Head>.Env(src/infra/env) lêprocess.envno server ewindow.__ENVno client.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 <Head><br/><script src='/api/env'>"] -->|"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-scriptsjustamente por isso:async/deferfariam os chunks do Next rodarem antes do env existir. O testesrc/infra/env/runtimeEnvScript.test.tsfalha 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_KEYSempages/api/env.tstem 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}pathe${API_URL}/pathjá quebrou phone auth e cadastro inicial em staging. - Falha é ruidosa. Sem
API_URLno pod,/api/envresponde 500 com umthrow; no browser,Env.getOrThrowlança nomeando o script que não carregou. Nunca voltar a devolverundefinedem 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
- Spec: 20260806163825_frontend_runtime_env_static_pages.md
- Learning: frontend_app_router_lib_in_pages_router.md
- Spec anterior, superseded: 20260806003006_frontend_runtime_api_proxy.md