Patrones de configuración de entorno para dev, staging y prod
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.

Por qué la configuración en duro sigue causando problemas
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:
- URLs base de la API incrustadas en la app (llamando a prod mientras pruebas, o a dev después del lanzamiento)
- Claves de API comprometidas en el repo (filtraciones, facturas inesperadas, rotaciones de emergencia)
- Toggles de funcionalidad escritos como constantes (tienes que entregar código para apagar algo)
- IDs de analítica y reporte de errores en duro (los datos terminan en el lugar equivocado)
“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:
- reglas claras sobre qué pertenece al código y qué a la configuración
- valores predeterminados seguros para dev, staging y prod
- switches de funcionalidades que puedan cambiar sin recompilar todo
- secretos gestionados fuera del código, con espacio para rotarlos
Qué realmente cambia entre dev, staging y prod
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.
Un modelo práctico de configuración que puedes reutilizar en todas partes
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):
- Valores predeterminados: valores seguros que funcionan en la mayoría de los sitios.
- Sobrescrituras por entorno: lo que cambia para dev, staging, prod.
- Secretos: valores sensibles que nunca viven en el repo.
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í:
- Los defaults viven en el código como constantes no sensibles (timeouts, tamaño de página, recuentos de reintentos).
- Las sobrescrituras viven en archivos específicos por entorno o variables de entorno (API base URL, analítica activada/desactivada).
- Los secretos viven en un almacén de secretos y se inyectan durante el deploy/build (JWT secret, contraseña de DB, claves de APIs de terceros).
Nombres que la gente pueda entender
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:
- Prefija por app: WEB_, API_, MOBILE_
- Usa MAYÚSCULAS_CON_GUIONES_BAJOS
- Agrupa por propósito: API_BASE_URL, AUTH_JWT_SECRET, FEATURES_NEW_CHECKOUT
- Mantén booleanos explícitos: FEATURES_SEARCH_ENABLED=true
Así nadie tiene que adivinar si “BASE_URL” es para la app React, el servicio Go o la app Flutter.
Paso a paso: configuración para la app web (React) sin hardcodear
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.
1) Decide qué es de build-time y qué de runtime
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.
2) Almacena la API base sin cometerla al repo
Mantén un archivo local para desarrolladores (no comprometido) y pon los valores reales en tu pipeline de deploy.
- Dev local: usa
.env.local(ignorarlo en git) con algo comoVITE_API_BASE_URL=http://localhost:8080 - CI/CD: configura
VITE_API_BASE_URLcomo variable de entorno en el job de build, o colócala en un archivo de configuración runtime creado durante el deploy
Ejemplo 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();
}
3) Expón solo valores seguros al navegador
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.
Paso a paso: configuración del backend (Go) que puedas validar
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.
Paso a paso: configuración móvil (Flutter) que se mantenga flexible
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.
1) Usa flavors para identidad, no para endpoints
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.json
En Dart, léelos con String.fromEnvironment y construye un AppConfig simple al inicio.
2) Pon URLs y switches en una configuración obtenida en runtime
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:
- Flavor (build-time): identidad de la app, URL por defecto de config, proyecto de crash reporting
- Remote config (runtime): API base URL, flags de funcionalidades, porcentajes de rollout, modo mantenimiento
- Secretos: nunca se publican en la app (los binarios móviles pueden inspeccionarse)
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.
Feature flags y switches que no se conviertan en caos
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.
Nombres que hagan la intención obvia
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.
Defaults, guardrails y limpieza
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:
- Cada flag tiene un owner y una fecha de “remover para”
- Elimina flags dentro de 1–2 releases después del rollout completo
- Registra valores de flags en flujos clave para depuración
- Revisa flags mensualmente como revisas dependencias
Secretos: almacenamiento, acceso y rotación básica
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.
Dónde deben vivir los secretos (por entorno)
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.
- Web (React): no pongas secretos en el navegador. Si el cliente necesita un token, usa un token de corta duración emitido por tu backend.
- Backend (Go): carga secretos desde variables de entorno o un secrets manager al inicio y mantenlos solo en memoria.
- Móvil (Flutter): trata la app como pública. Cualquier “secreto” en la app puede extraerse, así que usa tokens emitidos por el backend y almacenamiento seguro del dispositivo solo para datos de sesión del usuario.
Principios de rotación (sin romper producción)
La rotación falla cuando cambias una clave y olvidas que clientes viejos aún la usan. Planifica una ventana de solapamiento.
- Soporta dos secretos válidos a la vez (activo + previo) por una ventana corta.
- Rueda la nueva clave primero, luego cambia el puntero “activo”.
- Monitorea errores de autenticación y elimina la clave vieja tras la ventana.
- Registra versiones de secretos (no los valores) para depurar con seguridad.
Este enfoque de solapamiento funciona para claves de API, secretos de webhooks y claves de firma. Evita cortes sorpresa.
Ejemplo de rollout: cambiar URLs de API sin romper a los usuarios
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í:
- Despliega la API prod y mantenla “oscura” (solo tráfico interno) mientras staging sigue siendo la predeterminada.
- Cambia dependencias del backend primero (si tu backend llama a otros servicios), usando env vars y un reinicio rápido.
- Mueve el tráfico web en una porción pequeña (o solo cuentas internas).
- Publica la app móvil con la nueva configuración, pero mantén una flag controlada por servidor para retrasar el cambio hasta estar listos.
- Aumenta el tráfico gradualmente y mantén una ruta de rollback preparada.
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.
Lista rápida antes de lanzar
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.
- Las URLs base apuntan al lugar correcto (API, auth, CDN, pagos). Revisa web, backend y móvil por separado.
- No hay claves de prueba en producción y no hay claves de producción en dev o staging. También confirma que los nombres de las claves coincidan con lo que la app espera.
- Los feature flags tienen defaults seguros. Todo lo riesgoso debería venir por defecto apagado y habilitarse intencionalmente.
- La configuración de build y release coincide (bundle ID/nombre del paquete, dominio personalizado, orígenes CORS, URLs de redirect OAuth).
- La observabilidad está configurada (logs, reporte de errores, tracing) y etiquetada con el entorno correcto.
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.
- Abre la app y confirma que carga sin errores en la consola.
- Inicia sesión y realiza una llamada de API que requiera auth (perfil, ajustes o una lista de datos simple).
- Provoca una falla controlada (entrada inválida u offline) y confirma que ves un mensaje amigable, no una pantalla en blanco.
- Revisa logs y reporte de errores: un error de prueba debe aparecer bajo el entorno correcto en unos minutos.
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.
Errores comunes que llevan a outages
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.
Mezclar settings de build-time y runtime
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.
Enviar endpoints de dev o claves de prueba
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:
- Falla cerrado: si falta un valor requerido, falla temprano con un error claro.
- Haz que producción sea el entorno más difícil de configurar mal: no defaults de dev, no aceptar claves de prueba, no habilitar endpoints de depuración.
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.
Dejar que staging se aleje de 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.
Siguientes pasos: haz la configuración aburrida, repetible y segura
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:
- los valores obligatorios están presentes (no cadenas vacías)
- los tipos son correctos (no bugs de "true" vs true)
- las reglas solo-para-prod se cumplen (por ejemplo, HTTPS requerido)
- los feature flags tienen nombres conocidos (sin errores tipográficos)
- los secretos no están comprometidos en el repo
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.
Preguntas frecuentes
¿Qué debería ir en la configuración en lugar del código?
Mantén fuera de los archivos fuente los valores que cambian según el entorno. Deja los valores predeterminados no sensibles en el código, usa ajustes específicos de cada entorno para las URL y los interruptores, e inyecta los secretos durante la compilación o el despliegue.
¿Cuándo necesito un entorno nuevo?
Usa entornos independientes de desarrollo, pruebas y producción cuando necesites aislar datos, accesos o riesgos. Si solo cambia una URL, un límite o un interruptor, añade un valor de configuración.
¿Puedo almacenar secretos en una aplicación React?
No. Todo lo que se envía al navegador queda visible para los usuarios. Mantén las claves privadas y las contraseñas en el backend, y expón solo valores seguros, como una URL pública de API o una simple bandera de funcionalidad.
¿Mi aplicación web debería usar configuración en tiempo de compilación o de ejecución?
Usa valores de tiempo de compilación para ajustes que rara vez cambian y son seguros de exponer. Carga la configuración en tiempo de ejecución para valores que quizá debas cambiar después del despliegue, como una URL base de API o un interruptor de mantenimiento.
¿Cómo debería gestionar la configuración un backend de Go?
Carga las variables de entorno obligatorias en una estructura de configuración tipada al iniciar el servicio. Valida los entornos permitidos, las URL y las cadenas de conexión a la base de datos, y detén el inicio con un error claro si falta un valor obligatorio.
¿Cómo debería gestionar Flutter los entornos de desarrollo, pruebas y producción?
Usa los sabores de Flutter para los detalles específicos de la compilación, como el ID de la aplicación, la firma y el proyecto de analítica. Obtén ajustes no sensibles en tiempo de ejecución, como URL de API y banderas de funcionalidad, desde una fuente de configuración remota.
¿Cómo evito que se acumulen las banderas de funcionalidad?
Nombra las banderas según el comportamiento que habilitan, como feature.checkout_new_ui_enabled. Asigna a cada bandera un responsable y una fecha de eliminación, deja desactivados por defecto los cambios arriesgados y elimina la bandera poco después de un lanzamiento completo.
¿Dónde debería guardar las claves de API y las contraseñas de la base de datos?
Guarda los secretos de pruebas y producción en un almacén de secretos dedicado, e inyéctalos solo en los servicios que los necesitan. Conserva los secretos de desarrollo local en archivos locales ignorados o en el llavero del sistema operativo.
¿Cómo puedo rotar secretos sin causar tiempo de inactividad?
Mantén las credenciales antiguas y nuevas activas durante un breve período de solapamiento. Despliega los clientes o servicios con el nuevo valor, cambia el tráfico activo, vigila los fallos de autenticación y luego elimina la credencial antigua.
¿Qué debería comprobar antes de un lanzamiento a producción?
Comprueba que cada punto de conexión apunte al entorno de destino, que existan los secretos obligatorios y que las banderas arriesgadas estén desactivadas por defecto. Luego ejecuta un flujo de usuario completo en un perfil limpio del navegador o una instalación nueva de la aplicación y confirma que los registros usan la etiqueta de entorno correcta.