Variáveis de ambiente
Esta é a lista completa das variáveis que o backend do Load Balance lê do ambiente, com o default exato de cada uma, se o boot falha sem ela e o que ela controla. Use como referência de configuração e para auditar um deploy existente.
Como o boot valida#
Todas as variáveis passam por um schema Zod único, executado por validateEnv() no próprio import do módulo de configuração. Uma variável fora do schema não é lida; uma variável dentro do schema com valor inválido derruba o processo. Quando o ZodError acontece, a mensagem nomeia cada uma das variáveis culpadas.
Environment validation failed:
- DATABASE_URL: DATABASE_URL is required
Depois do parse, o código roda travas condicionais ao ambiente. Estas são as três que mudam o comportamento do boot:
DATABASE_URL é a única sem default: o Zod recusa string vazia em qualquer ambiente.OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, OIDC_POST_LOGOUT_REDIRECT_URI, CONTA_PROVISIONING_SECRET e APP_PUBLIC_URL. Quatro delas ainda precisam ser URL http(s) válida.AUTH_DISABLED=true, COOKIE_SECRET vazia ou curta, e CORS_ORIGINS vazia derrubam o boot.O motivo está no código: operadores esquecem env vars e o serviço sobe silenciosamente aberto. Um aviso passaria despercebido; um erro de boot aparece no log do orquestrador. Os testes de integração, por outro lado, não derrubam o processo: sem as variáveis TENANCY_IT_* eles rodam assim mesmo e o gate de skips é quem reprova.
Banco, servidor e infra#
A conexão com o Postgres, o cliente Redis, o pool e a escuta do servidor.
| Variável | Default | Obrigatória | O que controla |
|---|---|---|---|
DATABASE_URL | — | Sim | datasourceUrl do Prisma, com os parâmetros de pool injetados. Sem ela o boot falha em qualquer ambiente. |
REDIS_URL | redis://localhost:6379 | Não | Cliente Redis. |
DB_CONNECTION_LIMIT | 2000 | Não | connection_limit do Prisma, por instância. Decisão explícita do dono: nunca reduzir — 50 saturava com mais de 100 agentes simultâneos. |
DB_POOL_TIMEOUT | 30 (s) | Não | pool_timeout: quanto o Prisma espera por uma conexão livre antes de lançar. |
PORT | 3018 | Não | Porta do app.listen. Uma porta reservada pelo sistema é sobrescrita para 3018 com aviso no boot. |
NODE_ENV | production | Não | development, production ou test. Decide as travas de segurança. |
Portas reservadas que o servidor não aceita: 22, 3333, 5173, 5900, 6080 e 8443.
O max_connections do Postgres tem de comportar o limite de cada instância somado — multiplicado por 2 durante o deploy, quando a antiga e a nova convivem — mais os outros apps do mesmo servidor e uma margem para sessões admin.
Limites de payload e mídia#
O proxy aceita do cliente muito mais do que envia ao upstream, de propósito: ele precisa receber o body inteiro para podar a mídia antiga em vez de devolver 413.
| Variável | Default | Obrigatória | O que controla |
|---|---|---|---|
BODY_LIMIT_MB | 200 | Não | bodyLimit do Fastify: o que o proxy aceita do cliente. Fica bem acima do limite do upstream porque sessões de agente acumulam screenshots e passam dos 32 MB. |
UPSTREAM_MAX_BODY_MB | 30 | Não | O que o proxy envia ao upstream. Acima disso, as mídias mais antigas do histórico viram placeholder de texto. |
UPSTREAM_MAX_IMAGES | 100 | Não | Teto de imagens por request — a Anthropic recusa mais de 100 com 413/400. |
IMAGE_MAX_BASE64_MB | 5 | Não | Teto do base64 de uma imagem depois do resize. |
IMAGE_RESIZE_MAX_EDGE_PX | 1568 | Não | Lado maior da redução de imagem. O valor 0 desliga o resize. 1568 px é o ponto a partir do qual a Anthropic já reduz do lado dela. |
IMAGE_RESIZE_CACHE_MB | 128 | Não | Tamanho do cache de resize, em bytes de imagem. |
WS_MAX_PAYLOAD_MB | 10 | Não | maxPayload do WebSocket /ws/requests. 1 MB derruba lotes grandes sob alta concorrência. |
Proxy: cooldown, failover e streaming#
As variáveis que decidem quando uma chave sai da frente, quando volta e quando o pedido migra para a próxima.
| Variável | Default | Obrigatória | O que controla |
|---|---|---|---|
BILLING_COOLDOWN_MS | 60000 | Não | Pausa da chave depois de um 402. Um 402 nunca desativa a chave: ela só sai do rodízio por esta janela e volta sozinha. |
BILLING_RETRY_AFTER_SEC | 300 | Não | Valor do header Retry-After em cobrança esgotada. |
FIRST_CHUNK_TIMEOUT_MS | 15000(faixa 1000–120000) | Não | Espera pelo primeiro frame SSE válido antes de dar failover. Sem ela o proxy esperaria o timeout inteiro do upstream para decidir. |
HEDGE_DELAY_MS | 2000(faixa 500–30000) | Não | Atraso da reserva do hedge antes do primeiro token. |
STREAM_LOG_LIMIT_BYTES | 1048576(mín. 50000) | Não | Teto do coletor SSE. Acima de 50 KB um frame de erro que chega tarde era truncado antes de o coletor conseguir ver. |
REASONING_OWNER_TTL_SEC | 86400 | Não | Janela do registro que guarda qual chave cifrou cada bloco de raciocínio. Expirar só custa o replay do raciocínio. |
AUTO_DISABLE_ON_ERROR | true | Não | Kill-switch do auto-disable de chave em erro não-2xx. Com false o proxy ainda sanitiza a resposta, mas nenhuma chave é desativada. |
SAME_ERROR_GUARD_THRESHOLD | 3 | Não | Se o mesmo par status + mensagem atinge N chaves dentro de uma request, os auto-disables daquela request são revertidos. |
O guard existe por um motivo escrito no código: um nome de modelo digitado errado não pode derrubar 600 chaves de uma vez.
Rate limit e endereço do cliente#
| Variável | Default | Obrigatória | O que controla |
|---|---|---|---|
RATE_LIMIT_PROXY_IP_PER_MIN | 100 | Não | Rate limit do proxy por IP, por minuto. |
RATE_LIMIT_PROXY_URL_PER_MIN | 500 | Não | Rate limit do proxy por URL personalizada, por minuto. |
TRUST_PROXY_HOPS | 1 | Não | Quantos saltos de proxy à frente são confiáveis. Com trustProxy: n o X-Forwarded-For injetado além do salto confiável é ignorado, então um atacante não forja o bucket do rate limit girando o header. 0 desliga a confiança; em produção isso gera aviso no boot. |
Autenticação e sessão#
A sessão do painel e o segredo que assina o cookie e o token CSRF.
| Variável | Default | Obrigatória | O que controla |
|---|---|---|---|
AUTH_DISABLED | false | Sim em produção ser false | Com true desliga toda a autenticação admin. true em produção derruba o boot. |
SESSION_COOKIE_NAME | session_id | Não | Nome do cookie de sessão. |
COOKIE_SECRET | "" | Sim em produção | Assinatura do cookie de sessão e do token CSRF. Vazia em produção derruba o boot; curta demais (COOKIE_SECRET_MIN_LENGTH = 32) também. Fora de produção, vazia deriva o segredo do DATABASE_URL e o boot avisa — o GET /api/health expõe o modo como "env" ou "derived", nunca o valor. |
COOKIE_SECURE | ausente ⇒ NODE_ENV === 'production' | Não | Flag Secure do cookie. Sem valor explícito, desenvolvimento em HTTP local consegue gravar o cookie. |
COOKIE_SAMESITE | lax | Não | lax, strict ou none. strict quebra o callback OAuth que volta de outro domínio; none exige COOKIE_SECURE=true, senão o navegador rejeita o cookie. |
Configurar ou trocar o valor invalida uma vez todos os cookies assinados com o segredo anterior. O aviso está no comentário da própria variável.
OIDC / Conta Nommand#
O login é só pela Conta Nommand: não existe login próprio nem gestão de empresa no app. As sete variáveis abaixo são obrigatórias em qualquer ambiente — o boot falha nomeando as que faltam.
| Variável | Default | Obrigatória | O que controla |
|---|---|---|---|
OIDC_ISSUER | "" | Sim | Issuer da Conta, sem barra no fim. A discovery sai do issuer + /.well-known/openid-configuration. Precisa ser URL http(s). |
OIDC_CLIENT_ID | "" | Sim | client_id do app na Conta — o slug loadbalance também é a chave de apps[...] no id_token. |
OIDC_CLIENT_SECRET | "" | Sim | Segredo do cliente confidencial (client_secret_basic). Nunca vai para log. |
OIDC_REDIRECT_URI | "" | Sim | URL exata do callback cadastrada na Conta: o app + /auth/callback. Precisa ser URL http(s). |
OIDC_POST_LOGOUT_REDIRECT_URI | "" | Sim | Para onde a Conta volta depois do logout: o app + /auth/logged-out. Precisa ser URL http(s). |
CONTA_PROVISIONING_SECRET | "" | Sim | HMAC-SHA256 do webhook de provisionamento, verificado no header X-Conta-Signature. |
APP_PUBLIC_URL | "" | Sim | Origem pública do frontend. Precisa ser URL http(s); barra final é removida no boot. |
OIDC_REFRESH_INTERVAL_SEC | 600(clamp 30–600) | Não | Intervalo do refresh da sessão na Conta. O padrão de ≤ 10 min pega troca de papel e perda de acesso mesmo sem back-channel. |
O erro do boot sai na forma [AUTH] O login é pela Conta Nommand e exige OIDC_ISSUER, APP_PUBLIC_URL. Configure as variáveis da Conta Nommand. Recusando o boot. — a lista é montada a partir das variáveis que vieram vazias, não é uma lista fixa.
CORS e log#
| Variável | Default | Obrigatória | O que controla |
|---|---|---|---|
CORS_ORIGINS | "" | Sim em produção | Origens permitidas, separadas por vírgula. Vazia em produção derruba o boot; em development e test a ausência permite todas. |
CORS_ALLOW_ANY | false | Não | Escape hatch explícito: true força allow-all e imprime aviso. Não é recomendado. |
LOG_LEVEL | info | Não | Nível do logger Pino: fatal, error, warn, info, debug, trace ou silent. |
LOG_ENABLED | false | Não | Em produção o logger do Fastify é OFF por padrão. true liga o JSON estruturado sem mudar outro comportamento; em desenvolvimento o log é sempre ligado. |
DEBUG_LOGGING | false | Não | Log verboso. |
O servidor aceita a combinação allow-all com credenciais porque o aviso de app.ts a chama de silent open-CORS + admin-data-exfiltration. A alternativa honesta é listar as origens em CORS_ORIGINS.
Segredos em repouso e OAuth#
A cifragem das credenciais de provedor guardadas no banco e os fluxos OAuth contra o Anthropic e o GitHub.
| Variável | Default | Obrigatória | O que controla |
|---|---|---|---|
KEY_ENCRYPTION_KEY | "" | Não, mas é o que ativa a cifragem | Base64 de 32 bytes para AES-256-GCM de ApiKey.key, ApiKey.refreshToken e dos tokens dentro de ApiKey.metadata. Gerada com openssl rand -base64 32. Vazia ⇒ as credenciais são gravadas em texto puro, com aviso no boot. Presente e inválida ⇒ o boot falha em qualquer ambiente. |
KEY_ENCRYPTION_KEY_PREVIOUS | "" | Não | Chave anterior, só para rotação: linhas cifradas com a chave velha continuam legíveis depois da troca. Passa pela mesma validação de 32 bytes. |
OAUTH_CLIENT_ID | "" | Não | client_id do refresh contra console.anthropic.com/v1/oauth/token. Vazio ⇒ o fluxo OAuth da Anthropic é desligado e o boot avisa. Não existe default de produção de propósito. |
OAUTH_CLIENT_SECRET | ausente — sem default | Não | Segredo do fluxo OAuth da Anthropic. Sem default no schema. |
OAUTH_REDIRECT_URI | https://console.anthropic.com/oauth/code/callback | Não | Redirect do fluxo Anthropic. |
GITHUB_CLIENT_ID | "" | Não | Vazio desliga o fluxo GitHub OAuth — a string vazia é falsy e o gate do fluxo é !env.GITHUB_CLIENT_ID. |
GITHUB_CLIENT_SECRET | "" | Não | Segredo do fluxo GitHub OAuth; combinado com o GITHUB_CLIENT_ID no mesmo gate. |
GITHUB_REDIRECT_URI | "" | Não | Callback do fluxo GitHub. |
O aviso está escrito literalmente no schema: não há recuperação. Guarde a chave em cofre e use a mesma em todas as instâncias antes de rodar o backfill. Use KEY_ENCRYPTION_KEY_PREVIOUS para trocar a chave sem deixar as linhas antigas ilegíveis.
Com a chave presente, cada valor gravado leva o formato enc:v1:kid:iv:tag:ct e um AAD que amarra o valor à linha e ao campo (loadbalance:apikey:id:campo). Se a chave faltar no boot, assertSecretsReadable() varre a tabela api_keys e derruba o processo citando o id da linha, nunca o valor.
Retenção e limpeza#
Cada janela tem o próprio timer ou o próprio lote. O audit não tem intervalo próprio: ele roda dentro do timer de 6 h do cleanup, com a janela dele.
| Variável | Default | Obrigatória | O que controla |
|---|---|---|---|
RETENTION_DAYS | 30 | Não | Janela de requests. |
AUDIT_RETENTION_DAYS | 365 | Não | Audit log — responde “quem fez isso” meses depois, por isso vive bem mais que os requests. |
REQUEST_PAYLOAD_RETENTION_HOURS | 72 | Não | Corpo gzip e metadata completa do request. Separada da RETENTION_DAYS porque o payload é caro e só serve para a tela de detalhe. |
REQUEST_METRICS_ROLLUP_LOOKBACK_HOURS | 24 | Não | Janela que o job de rollup cobre desde o boot. É informativo: o job é idempotente e não guarda estado. |
CLEANUP_BATCH_SIZE | 1000 | Não | Tamanho do lote de DELETE do cleanup. Reduza se o lote causar contenção de lock. |
COMPRESSION_GZIP_LEVEL | 6 | Não | Nível de gzip dos corpos armazenados, de 0 (rápido) a 9 (menor volume). |
Observabilidade e alerta#
Cada integração só inicializa quando a variável dela está setada — sem import, sem hook, zero overhead quando não configurada. Se a variável aponta para um SDK que não está instalado, o boot registra uma linha de aviso e o app segue normal.
| Variável | Default | Obrigatória | O que controla |
|---|---|---|---|
SENTRY_DSN | "" | Não | Vazia desliga o Sentry: a inicialização faz early-return. |
SENTRY_ENVIRONMENT | "" ⇒ NODE_ENV | Não | environment do Sentry. |
SENTRY_TRACES_SAMPLE_RATE | 0 | Não | Fração de transações rastreadas. 0 reporta só erros, sem tracing. |
OTEL_EXPORTER_OTLP_ENDPOINT | "" | Não | Endpoint OTLP/HTTP de traces. Vazio desliga o OpenTelemetry: a inicialização faz early-return. |
OTEL_SERVICE_NAME | ai-proxy-loadbalancer | Não | serviceName enviado ao coletor OTel. |
ALERT_WEBHOOK_URL | "" | Não | Vazio desliga o alerta. Com URL, o proxy dispara em quatro eventos: trip do [GUARD], circuit-breaker OAuth em OPEN, falha permanente de OAuth e auto-disable de chave. O payload é aceito por Slack incoming webhooks e PagerDuty Events API v2. |
ALERT_DEDUP_MS | 30000 | Não | Janela de dedup por par evento + chave. O primeiro evento da janela dispara, os seguintes são coalescidos; 0 desliga o dedup. |
Só de teste e fora do schema#
Estas variáveis não são lidas pelo backend em execução. Elas aparecem no backend/.env.example, que é documento e não configuração.
| Variável | Default | Obrigatória | O que controla |
|---|---|---|---|
TENANCY_IT_DATABASE_URL | — | Só para npm run test:it | Banco descartável da integração. Sem ela os arquivos de segurança abrem com describe.skipIf e o npm test sai 0 com todos eles pulados. |
TENANCY_IT_REDIS_URL | — | Só para npm run test:it | Redis do mesmo suite. Opcional para o script, mas o gate de skips reprova com ela ausente também. |
ADMIN_API_KEY | — | Não | Obsoleta. Saiu do schema; se ainda existir no deploy, é ignorada. Só sessão de usuário autentica. |
Próximos passos#
Você tem o mapa completo do que o ambiente controla. Para ver cada uma dessas variáveis em ação num deploy real, e a ordem de boot que respeita, siga para o guia de operação.
Depois de dominar o básico, explore além: