Padrões de configuração de ambiente para manter URLs, chaves e feature flags fora do código em web, backend e mobile para dev, staging e prod.

Configuração hardcoded parece funcionar no primeiro dia. Aí você precisa de um ambiente de staging, uma segunda API, ou um ajuste rápido de feature, e a mudança “simples” vira um risco de release. A solução é direta: mantenha valores de ambiente fora dos arquivos fonte e coloque-os em uma organização previsível.
Os culpados mais comuns são fáceis de identificar:
"Só mude isso pra prod" cria o hábito de edições de última hora. Essas edições muitas vezes pulam revisão, testes e repetibilidade. Uma pessoa muda uma URL, outra muda uma chave, e agora você não consegue responder uma pergunta básica: qual configuração exata veio com esse build?
Um cenário comum: você constrói uma nova versão móvel contra staging, então alguém troca a URL para prod pouco antes do release. O backend muda de novo no dia seguinte, e é preciso dar rollback. Se a URL está hardcoded, o rollback exige outra atualização do app. Usuários esperam, e tickets de suporte se acumulam.
O objetivo aqui é um esquema simples que funcione em um app web, um backend Go e um app mobile Flutter:
Dev, staging e prod devem parecer o mesmo app rodando em três lugares diferentes. A ideia é trocar valores, não comportamento.
O que deve mudar é qualquer coisa ligada ao lugar onde o app roda ou a quem o usa: URLs base e hostnames, credenciais, integrações sandbox vs reais, e controles de segurança como nível de logs ou configurações mais rígidas em prod.
O que deve permanecer igual é a lógica e o contrato entre partes. Rotas da API, formatos de requisição/resposta, nomes de features e regras de negócio centrais não devem variar por ambiente. Se o staging se comporta diferente, deixa de ser um ensaio confiável para produção.
Uma regra prática para “novo ambiente” vs “novo valor de config”: crie um novo ambiente apenas quando precisar de um sistema isolado (dados separados, acesso e risco). Se você só precisa de endpoints diferentes ou números distintos, adicione um valor de config.
Exemplo: quer testar um novo provedor de busca. Se é seguro habilitá‑lo para um grupo pequeno, mantenha um staging e acrescente uma feature flag. Se exige um banco de dados separado e controles de acesso rigorosos, aí sim faz sentido um novo ambiente.
Uma boa configuração faz uma coisa bem: torna difícil enviar acidentalmente uma URL de dev, uma chave de teste, ou uma feature inacabada.
Use as mesmas três camadas para todo app (web, backend, mobile):
Para evitar confusão, escolha uma fonte de verdade por app e mantenha‑a. Por exemplo, o backend lê variáveis de ambiente na inicialização, o app web lê variáveis de build ou um pequeno arquivo de config em runtime, e o app mobile lê um pequeno arquivo de ambiente selecionado no build. Consistência dentro de cada app importa mais do que forçar o mesmo mecanismo em todos.
Um esquema simples e reutilizável fica assim:
Dê a cada item de config um nome claro que responda três perguntas: o que é, onde se aplica e que tipo é.
Uma convenção prática:
Assim, ninguém precisa adivinhar se “BASE_URL” é para o app React, o serviço Go ou o app Flutter.
Código React roda no navegador do usuário, então tudo o que você envia pode ser lido. O objetivo é simples: mantenha segredos no servidor e exponha ao navegador apenas configurações “seguras” como API base URL, nome do app ou toggles não sensíveis.
Config de build é injetada quando você constrói o bundle. Serve para valores que raramente mudam e que é seguro expor.
Config de runtime é carregada quando o app inicia (por exemplo, de um pequeno arquivo JSON servido com o app ou de um global injetado). É melhor para valores que você pode querer alterar depois do deploy, como trocar uma API base URL entre ambientes.
Uma regra simples: se mudar não deveria requerer rebuild da UI, faça em runtime.
Mantenha um arquivo local para desenvolvedores (não commitado) e defina valores reais no pipeline de deploy.
.env.local (gitignored) com algo como VITE_API_BASE_URL=http://localhost:8080VITE_API_BASE_URL como variável de ambiente no job de build, ou coloque num arquivo de config em runtime criado durante o deployExemplo de runtime (servido junto ao app):
{ "apiBaseUrl": "https://api.staging.example.com", "features": { "newCheckout": false } }
Depois carregue isso uma vez na inicialização e mantenha em um único lugar:
export async function loadConfig() {
const res = await fetch('/config.json', { cache: 'no-store' });
return res.json();
}
Trate tudo em env vars do React como público. Não coloque senhas, chaves privadas ou URLs de banco no app web.
Exemplos seguros: API base URL, Sentry DSN (público), versão do build e flags simples de feature.
Config do backend fica mais segura quando tipada, carregada de variáveis de ambiente e validada antes de o servidor começar a aceitar tráfego.
Comece decidindo o que o backend precisa para rodar e torne esses valores explícitos. Valores “must have” típicos são:
APP_ENV (dev, staging, prod)HTTP_ADDR (por exemplo :8080)DATABASE_URL (DSN do Postgres)PUBLIC_BASE_URL (usada em callbacks e links)API_KEY (para serviço de terceiros)Então carregue‑os em uma struct e falhe rápido se algo estiver faltando ou malformado. Assim você encontra problemas em segundos, não depois de um deploy parcial.
package config
import (
"errors"
"net/url"
"os"
"strings"
)
type Config struct {
Env string
HTTPAddr string
DatabaseURL string
PublicBaseURL string
APIKey string
}
func Load() (Config, error) {
c := Config{
Env: mustGet("APP_ENV"),
HTTPAddr: getDefault("HTTP_ADDR", ":8080"),
DatabaseURL: mustGet("DATABASE_URL"),
PublicBaseURL: mustGet("PUBLIC_BASE_URL"),
APIKey: mustGet("API_KEY"),
}
return c, c.Validate()
}
func (c Config) Validate() error {
if c.Env != "dev" && c.Env != "staging" && c.Env != "prod" {
return errors.New("APP_ENV must be dev, staging, or prod")
}
if _, err := url.ParseRequestURI(c.PublicBaseURL); err != nil {
return errors.New("PUBLIC_BASE_URL must be a valid URL")
}
if !strings.HasPrefix(c.DatabaseURL, "postgres://") {
return errors.New("DATABASE_URL must start with postgres://")
}
return nil
}
func mustGet(k string) string {
v, ok := os.LookupEnv(k)
if !ok || strings.TrimSpace(v) == "" {
panic("missing env var: " + k)
}
return v
}
func getDefault(k, def string) string {
if v, ok := os.LookupEnv(k); ok && strings.TrimSpace(v) != "" {
return v
}
return def
}
Isso mantém DSNs de banco, chaves de API e URLs de callback fora do código e fora do git. Em setups hospedados, você injeta essas variáveis de ambiente por ambiente para que dev, staging e prod possam diferir sem alterar uma única linha.
Apps Flutter geralmente precisam de duas camadas de config: flavors em build (o que você entrega) e configurações em runtime (o que o app pode mudar sem novo release). Manter essas camadas separadas evita que “só trocar uma URL” vire um rebuild de emergência.
Crie três flavors: dev, staging, prod. Flavors devem controlar coisas que precisam ser fixas em build, como nome do app, bundle id, assinatura, projeto de analytics e se ferramentas de debug estão habilitadas.
Passe apenas defaults não sensíveis com --dart-define (ou pelo CI) para nunca hardcodar no código:
ENV=stagingDEFAULT_API_BASE=https://api-staging.example.comCONFIG_URL=https://config.example.com/mobile.jsonEm Dart, leia com String.fromEnvironment e construa um AppConfig simples no startup.
Se quiser evitar rebuilds por pequenas mudanças de endpoint, não trate a API base URL como constante. Busque um pequeno arquivo de config no lançamento do app (e faça cache). O flavor define apenas de onde buscar a config.
Uma divisão prática:
Se mover o backend, atualize a remote config para apontar para a nova base URL. Usuários existentes pegam na próxima abertura, com fallback seguro para o último valor em cache.
Feature flags servem para rollouts graduais, testes A/B, kill switches rápidos e testar mudanças arriscadas em staging antes de ligar em prod. Elas não substituem controles de segurança. Se uma flag protege algo que precisa ser protegido, não é flag — é regra de autorização.
Trate cada flag como uma API: nome claro, um responsável e uma data de término.
Use nomes que digam o que acontece quando a flag está ON e qual parte do produto ela toca. Um esquema simples:
feature.checkout_new_ui_enabled (para clientes)ops.payments_kill_switch (chave de desligar em emergência)exp.search_rerank_v2 (experimento)release.api_v3_rollout_pct (rollout gradual)debug.show_network_logs (diagnósticos)Prefira booleanos positivos (..._enabled) em vez de duplas negativas. Mantenha um prefixo estável para facilitar busca e auditoria.
Comece com defaults seguros: se o serviço de flags cair, seu app deve se comportar como a versão estável.
Um padrão realista: envie um novo endpoint no backend, mantenha o antigo rodando e use release.api_v3_rollout_pct para mover tráfego gradualmente. Se erros subirem, volte sem hotfix.
Para evitar acúmulo de flags, siga algumas regras:
Um “segredo” é qualquer coisa que causaria dano se vazasse. Pense em tokens de API, senhas de banco, segredos de cliente OAuth, chaves de assinatura (JWT), segredos de webhook e certificados privados. Não são segredos: URLs base de API, números de build, feature flags ou IDs públicos de analytics.
Separe segredos do resto das suas configurações. Desenvolvedores devem poder mudar config segura livremente, enquanto segredos são injetados apenas em runtime e apenas onde necessário.
No dev, mantenha segredos locais e descartáveis. Use um arquivo .env ou o keychain do SO e facilite o reset. Nunca commit.
Em staging e prod, segredos devem ficar num cofre dedicado, não no repo, não em logs de chat e não embutidos em apps móveis.
A rotação falha quando você troca uma chave e esquece que clientes antigos ainda a usam. Planeje uma janela de sobreposição.
Essa abordagem de sobreposição funciona para chaves de API, segredos de webhook e chaves de assinatura. Evita outages surpresa.
Você tem uma API de staging e uma nova API de produção. O objetivo é mover tráfego em fases, com um retorno rápido se algo der errado. Isso é mais fácil quando o app lê a API base URL da config, não do código.
Trate a URL da API como um valor de deploy em todo lugar. No web app (React), costuma ser valor de build ou arquivo de config em runtime. No mobile (Flutter), tipicamente flavor + remote config. No backend (Go), variável de ambiente em runtime. O importante é consistência: o código usa um único nome de variável (por exemplo, API_BASE_URL) e nunca embute a URL em componentes, serviços ou telas.
Um rollout faseado seguro pode ser assim:
A verificação é sobre pegar incompatibilidades cedo. Antes de usuários reais atingirem a mudança, confirme endpoints de health, fluxos de auth e que uma conta de teste consegue completar uma jornada chave do começo ao fim.
A maioria dos bugs de configuração em produção é chata: um valor de staging deixado, um default de flag invertido ou uma chave de API faltando numa região. Uma checagem rápida pega a maioria.
Antes de deploy, confirme três coisas que batam com o ambiente alvo: endpoints, segredos e defaults.
Depois faça um smoke test rápido. Escolha um fluxo real de usuário e rode end to end, usando instalação fresca ou perfil limpo do navegador para não depender de tokens em cache.
Um hábito prático: trate staging como produção com valores diferentes. Isso significa o mesmo esquema de config, as mesmas regras de validação e a mesma forma de deploy. Só os valores mudam.
A maioria das falhas de configuração não é exótica. São erros simples que passam porque a config está espalhada por arquivos, passos de build e dashboards, e ninguém sabe: “Quais valores esse app vai usar agora?” Uma boa configuração facilita responder essa pergunta.
Uma armadilha comum é colocar valores de runtime em lugares de build-time. Embutir uma API base URL numa build React significa rebuild para cada ambiente. Aí alguém deploya o artefato errado e a produção aponta para staging.
Uma regra mais segura: só bake em valores que realmente nunca mudam após release (como versão do app). Mantenha detalhes de ambiente (API URLs, switches de feature, endpoints de analytics) em runtime quando possível, e deixe clara a fonte de verdade.
Isso acontece quando defaults são “úteis” mas inseguros. Um app mobile pode defaultar para uma API de dev se não conseguir ler a config, ou um backend pode cair para um banco local se uma env var faltar. Isso transforma um pequeno erro de config em uma queda completa.
Duas práticas ajudam:
Um exemplo realista: um release vai na sexta à noite e o build de produção contém por engano uma chave de pagamento de staging. Tudo “funciona” até cobranças falharem silenciosamente. A correção não é uma nova biblioteca de pagamentos. É validação que rejeita chaves não‑prod em produção.
Staging que não reflete produção dá confiança falsa. Configs de banco diferentes, jobs de background faltando ou flags extras fazem bugs aparecer só depois do lançamento.
Mantenha staging perto espelhando o mesmo esquema de config, as mesmas regras de validação e a mesma forma de deploy. Só os valores devem diferir, não a estrutura.
O objetivo não é ferramenta sofisticada. É consistência chata: os mesmos nomes, os mesmos tipos, as mesmas regras entre dev, staging e prod. Quando config é previsível, releases deixam de ser arriscados.
Comece escrevendo um contrato de configuração claro em um lugar. Mantenha curto mas específico: cada nome de chave, seu tipo (string, número, boolean), de onde pode vir (env var, remote config, build-time) e seu default. Adicione notas para valores que nunca devem ser colocados em um app cliente (como chaves privadas). Trate esse contrato como uma API: mudanças precisam de revisão.
Depois faça erros falharem cedo. O melhor momento para descobrir uma URL de API faltando é no CI, não depois do deploy. Adicione validação automatizada que carregue a config do mesmo jeito que seu app e verifique:
Finalmente, facilite a recuperação quando uma mudança de config der errado. Snapshot do que está rodando, mude uma coisa por vez, verifique rápido e mantenha caminho de rollback.
Se você está construindo e deployando com uma plataforma como Koder.ai (koder.ai), as mesmas regras se aplicam: trate valores de ambiente como entradas para build e hosting, mantenha segredos fora do código exportado e valide a config antes de shipar. Essa consistência é o que faz redeploys e rollbacks parecerem rotina.
Quando a config está documentada, validada e reversível, ela deixa de ser fonte de outages e vira uma parte normal do shipping.