Patrones de configuración de entorno que mantienen URLs, claves y flags fuera del código en web, backend y móvil para dev, staging y prod.

La configuración en duro parece bien el primer día. Luego necesitas un entorno de staging, una segunda API o un cambio rápido de funcionalidad, y el “cambio simple” se convierte en un riesgo de release. La solución es directa: mantén los valores de entorno fuera de los archivos fuente y ponlos en una configuración predecible.
Los culpables habituales son fáciles de ver:
“Solo cámbialo para prod” crea el hábito de ediciones de último minuto. Esas ediciones a menudo saltan revisiones, pruebas y repetibilidad. Una persona cambia una URL, otra cambia una clave, y ahora no puedes responder una pregunta básica: ¿qué configuración exacta se incluyó en este build?
Un escenario común: construyes una versión móvil nueva contra staging, luego alguien cambia la URL a prod justo antes del lanzamiento. El backend cambia al día siguiente y necesitas revertir. Si la URL está en duro, revertir significa otra actualización de la app. Los usuarios esperan y las tickets de soporte se acumulan.
El objetivo aquí es un esquema simple que funcione en una app web, un backend en Go y una app móvil en Flutter:
Dev, staging y prod deberían sentirse como la misma app corriendo en tres lugares distintos. La idea es cambiar valores, no comportamiento.
Lo que debe cambiar es cualquier cosa ligada a dónde corre la app o quién la usa: URLs base y hostnames, credenciales, integraciones de sandbox vs reales y controles de seguridad como nivel de logs o ajustes más estrictos en prod.
Lo que debe permanecer igual es la lógica y el contrato entre partes. Rutas de API, formas de request y response, nombres de funcionalidades y reglas de negocio centrales no deberían variar por entorno. Si staging se comporta distinto, deja de ser un ensayo confiable para producción.
Una regla práctica para “nuevo entorno” vs “nuevo valor de configuración”: crea un nuevo entorno solo cuando necesites un sistema aislado (datos, accesos y riesgos separados). Si solo necesitas endpoints distintos o valores distintos, añade un valor de configuración en su lugar.
Ejemplo: quieres probar un nuevo proveedor de búsqueda. Si es seguro activarlo para un grupo pequeño, mantén un solo entorno de staging y añade una feature flag. Si requiere una base de datos separada y controles de acceso estrictos, entonces vale la pena un nuevo entorno.
Una buena configuración hace una cosa bien: dificulta que por accidente entregues una URL de dev, una clave de prueba o una funcionalidad no terminada.
Usa las mismas tres capas para cada app (web, backend, móvil):
Para evitar confusiones, elige una fuente de verdad por app y mantenla. Por ejemplo, el backend lee variables de entorno al inicio, la app web lee variables en build o un pequeño archivo de configuración en tiempo de ejecución, y la app móvil lee un pequeño archivo de entorno seleccionado en tiempo de build. La consistencia dentro de cada app importa más que forzar el mismo mecanismo exacto en todo.
Un esquema simple y reutilizable se ve así:
Da a cada ítem de configuración un nombre claro que responda tres preguntas: qué es, dónde aplica y de qué tipo es.
Una convención práctica:
Así nadie tiene que adivinar si “BASE_URL” es para la app React, el servicio Go o la app Flutter.
El código React se ejecuta en el navegador del usuario, así que todo lo que publiques puede leerse. La meta es simple: mantener secretos en el servidor y dejar que el navegador lea solo ajustes “seguros” como la URL base de la API, el nombre de la app o un toggle de característica no sensible.
La configuración de build-time se inyecta cuando generas el bundle. Está bien para valores que cambian raramente y que es seguro exponer.
La configuración de runtime se carga cuando la app arranca (por ejemplo, desde un pequeño archivo JSON servido con la app o un global inyectado). Es mejor para valores que quieras cambiar después del deploy, como alternar una URL base de API entre entornos.
Una regla simple: si cambiarlo no debería requerir reconstruir la UI, hazlo runtime.
Mantén un archivo local para desarrolladores (no comprometido) y pon los valores reales en tu pipeline de deploy.
.env.local (ignorarlo en git) con algo como VITE_API_BASE_URL=http://localhost:8080VITE_API_BASE_URL como variable de entorno en el job de build, o colócala en un archivo de configuración runtime creado durante el deployEjemplo runtime (servido junto a tu app):
{ "apiBaseUrl": "https://api.staging.example.com", "features": { "newCheckout": false } }
Luego cárgalo una vez al inicio y guárdalo en un solo lugar:
export async function loadConfig() {
const res = await fetch('/config.json', { cache: 'no-store' });
return res.json();
}
Trata todo en las env vars de React como público. No pongas contraseñas, claves privadas de API o URLs de bases de datos en la app web.
Ejemplos seguros: API base URL, Sentry DSN (público), versión de build y flags simples de funcionalidad.
La configuración del backend se mantiene más segura cuando está tipada, se carga desde variables de entorno y se valida antes de que el servidor empiece a aceptar tráfico.
Empieza por decidir qué necesita el backend para funcionar y haz esos valores explícitos. Valores típicos “obligatorios” son:
APP_ENV (dev, staging, prod)HTTP_ADDR (por ejemplo :8080)DATABASE_URL (DSN de Postgres)PUBLIC_BASE_URL (usada para callbacks y enlaces)API_KEY (para un servicio de terceros)Luego cárgalos en una estructura y falla rápido si falta o está mal formado. Así encuentras problemas en segundos, no después de un 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
}
Esto mantiene DSNs de bases de datos, claves de API y URLs de callback fuera del código y fuera de git. En entornos hospedados, inyectas estas variables por entorno para que dev, staging y prod puedan diferir sin cambiar una sola línea.
Las apps Flutter suelen necesitar dos capas de configuración: flavors en build-time (lo que publicas) y ajustes runtime (lo que la app puede cambiar sin una nueva release). Mantenerlos separados evita que “solo un cambio rápido de URL” se convierta en una reconstrucción de emergencia.
Crea tres flavors: dev, staging, prod. Los flavors deben controlar cosas que deben fijarse en build time, como nombre de la app, bundle id, firma, proyecto de analítica y si las herramientas de depuración están habilitadas.
Luego pasa solo defaults no sensibles con --dart-define (o tu CI) para que nunca los hardcodees en el código:
ENV=stagingDEFAULT_API_BASE=https://api-staging.example.comCONFIG_URL=https://config.example.com/mobile.jsonEn Dart, léelos con String.fromEnvironment y construye un AppConfig simple al inicio.
Si quieres evitar reconstruir por pequeños cambios de endpoints, no trates la API base como una constante. Recupera un pequeño archivo de config al iniciar la app (y cachealo). El flavor solo establece desde dónde obtener la config.
Una división práctica:
Si mueves tu backend, actualizas la remote config para apuntar al nuevo base URL. Los usuarios existentes lo recibirán en el próximo lanzamiento, con un fallback seguro al último valor cacheado.
Los feature flags son útiles para despliegues graduales, pruebas A/B, interruptores de emergencia y probar cambios riesgosos en staging antes de activarlos en prod. No reemplazan controles de seguridad. Si un flag protege algo que debe estar protegido, no es un flag: es una regla de autorización.
Trata cada flag como una API: nombre claro, un propietario y una fecha de fin.
Usa nombres que digan qué pasa cuando la flag está ON y qué parte del producto toca. Un esquema simple:
feature.checkout_new_ui_enabled (funcionalidad visible al cliente)ops.payments_kill_switch (interruptor de apagado de emergencia)exp.search_rerank_v2 (experimento)release.api_v3_rollout_pct (despliegue gradual)debug.show_network_logs (diagnóstico)Prefiere booleanos positivos (..._enabled) sobre dobles negativos. Mantén un prefijo estable para poder buscar y auditar flags.
Empieza con defaults seguros: si el servicio de flags cae, tu app debería comportarse como la versión estable.
Un patrón realista: publica un endpoint nuevo en el backend, mantén el antiguo en funcionamiento y usa release.api_v3_rollout_pct para mover tráfico gradualmente. Si las fallas suben, vuelve atrás sin hotfix.
Para evitar acumulación de flags, sigue unas reglas:
Un “secreto” es cualquier cosa que cause daño si se filtra. Piensa en tokens de API, contraseñas de bases de datos, secretos de clientes OAuth, claves de firma (JWT), secretos de webhooks y certificados privados. No son secretos: URLs base de APIs, números de build, feature flags o IDs públicos de analítica.
Separa los secretos del resto de la configuración. Los desarrolladores deben poder cambiar config segura libremente, mientras que los secretos se inyectan solo en runtime y solo donde se necesiten.
En dev, mantiene secretos locales y desechables. Usa un archivo .env o el llavero del OS y haz que sea fácil restablecerlos. Nunca lo cometas.
En staging y prod, los secretos deben vivir en un almacén de secretos dedicado, no en el repo de código, no en chats y no incluidos en binarios móviles.
La rotación falla cuando cambias una clave y olvidas que clientes viejos aún la usan. Planifica una ventana de solapamiento.
Este enfoque de solapamiento funciona para claves de API, secretos de webhooks y claves de firma. Evita cortes sorpresa.
Tienes una API de staging y una nueva API de producción. El objetivo es mover tráfico por fases, con una forma rápida de volver si algo falla. Esto es más fácil cuando la app lee la API base URL desde config, no desde código.
Trata la URL de la API como un valor de deploy en todas partes. En la web (React), suele ser un valor de build o un archivo de config runtime. En móvil (Flutter), típicamente es un flavor más remote config. En backend (Go), es una variable de entorno en runtime. Lo importante es la consistencia: el código usa un único nombre de variable (por ejemplo, API_BASE_URL) y nunca embebe la URL en componentes, servicios o pantallas.
Un despliegue por fases seguro puede verse así:
La verificación consiste en detectar desaciertos temprano. Antes de que usuarios reales reciban el cambio, confirma que los endpoints de salud respondan, los flujos de autenticación funcionen y que una cuenta de prueba pueda completar un viaje clave de extremo a extremo.
La mayoría de bugs de configuración en producción son aburridos: un valor de staging dejado, un default de flag invertido o una clave de API faltante en una región. Un pase rápido detecta la mayoría.
Antes de desplegar, confirma que tres cosas coinciden con el entorno objetivo: endpoints, secretos y defaults.
Luego haz una prueba rápida de humo. Elige un flujo real de usuario y ejecútalo de extremo a extremo con una instalación fresca o un perfil de navegador limpio para no depender de tokens cacheados.
Un hábito práctico: trata staging como producción con valores distintos. Eso significa el mismo esquema de config, las mismas reglas de validación y la misma forma de despliegue. Solo los valores deben diferir.
La mayoría de outages por configuración no son exóticos. Son errores simples que se cuelan porque la config está repartida en archivos, pasos de build y dashboards, y nadie puede responder: “¿Qué valores usará esta app ahora mismo?” Una buena configuración facilita esa pregunta.
Una trampa común es poner valores de runtime en lugares de build-time. Incrustar una API base URL en un build de React significa que debes reconstruir para cada entorno. Entonces alguien despliega el artefacto equivocado y producción apunta a staging.
Una regla más segura: solo incrusta valores que realmente nunca cambian tras el release (como la versión de la app). Mantén detalles de entorno (URLs de API, switches de features, endpoints de analítica) en runtime cuando sea posible y haz obvia la fuente de verdad.
Esto ocurre cuando los defaults son “útiles” pero inseguros. Una app móvil puede apuntar por defecto a una API de dev si no puede leer la config, o un backend puede caer a una DB local si falta una variable. Eso convierte un pequeño error de config en un outage completo.
Dos hábitos ayudan:
Un ejemplo realista: un release sale un viernes por la noche y el build de producción contiene por accidente una clave de pago de staging. Todo “funciona” hasta que los cobros fallan silenciosamente. La solución no es una librería de pagos nueva: es validación que rechace claves no productivas en producción.
Staging que no coincide con producción da una falsa confianza. Diferencias en configuración de DB, tareas en segundo plano faltantes o flags extras hacen que los errores aparezcan solo después del lanzamiento.
Mantén staging cercano reflejando el mismo esquema de config, las mismas reglas de validación y la misma forma de despliegue. Solo los valores deben cambiar, no la estructura.
El objetivo no es una herramienta elegante. Es consistencia aburrida: los mismos nombres, los mismos tipos, las mismas reglas en dev, staging y prod. Cuando la configuración es predecible, los lanzamientos dejan de ser riesgosos.
Empieza escribiendo un contrato de configuración claro en un solo lugar. Mantenlo corto pero específico: cada nombre de clave, su tipo (string, number, boolean), de dónde puede venir (env var, remote config, build-time) y su valor por defecto. Agrega notas para valores que nunca deben estar en una app cliente (como claves privadas de API). Trata este contrato como una API: los cambios requieren revisión.
Luego haz que los errores fallen temprano. El mejor momento para descubrir que falta una API base URL es en CI, no después del deploy. Añade validación automática que cargue la configuración de la misma manera que tu app y verifique:
Finalmente, facilita la recuperación cuando un cambio de config sale mal. Haz snapshot de lo que está corriendo, cambia una cosa a la vez, verifica rápido y mantén una vía de rollback.
Si construyes y despliegas con una plataforma como Koder.ai (koder.ai), se aplican las mismas reglas: trata los valores de entorno como entradas para build y hosting, mantiene secretos fuera del código exportado y valida la configuración antes de publicar. Esa consistencia es lo que hace que redeploys y rollbacks se sientan rutinarios.
Cuando la configuración está documentada, validada y es reversible, deja de ser una fuente de outages y se convierte en una parte normal del envío.