ADR-015 — Configuración por entorno con String.fromEnvironment¶
| Campo | Valor |
|---|---|
| Estado | Aceptado |
| Fecha | 26 de febrero de 2026 |
| Decisores | Equipo Custodiam (Marcos Val Sanz, Rodrigo Mulero García) |
Contexto¶
La app Flutter consume al menos dos servicios externos cuyas URLs cambian entre entornos:
API_BASE_URL— host decustodiam-api(en local:http://localhost:8000/api/v1; en producción:https://api.custodiam.es/api/v1).KEYCLOAK_BASE_URL— host de Keycloak (en local:http://localhost:8080; en producción:https://auth.custodiam.es).
Las URLs no pueden vivir hardcoded en el código fuente. Hay que decidir dónde residen y cómo se inyectan en cada build.
Decisión¶
Clase EnvConfig en lib/core/config/env_config.dart con String.fromEnvironment + inyección por --dart-define en el build.
class EnvConfig {
static const String apiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://localhost:8000/api/v1',
);
static const String keycloakBaseUrl = String.fromEnvironment(
'KEYCLOAK_BASE_URL',
defaultValue: 'http://localhost:8080',
);
}
Builds:
# Desarrollo local (usa defaults a localhost)
flutter run
# Build release a stores
flutter build apk \
--dart-define=API_BASE_URL=https://api.custodiam.es/api/v1 \
--dart-define=KEYCLOAK_BASE_URL=https://auth.custodiam.es
flutter build ios --release \
--dart-define=API_BASE_URL=https://api.custodiam.es/api/v1 \
--dart-define=KEYCLOAK_BASE_URL=https://auth.custodiam.es
# Build web producción
flutter build web \
--dart-define=API_BASE_URL=https://api.custodiam.es/api/v1 \
--dart-define=KEYCLOAK_BASE_URL=https://auth.custodiam.es
No se hardcodean URLs ni identificadores de entorno en lib/, AndroidManifest.xml, Info.plist ni Runner.entitlements.
Justificación¶
-
Mínimo viable, sin JSON files innecesarios. En MVP, los dos entornos relevantes son "local" y "producción". Un sistema con archivos JSON por entorno (
dev.json,staging.json,prod.json) tiene sentido cuando hay 3-5 entornos con configuración rica. Para 2 entornos con 2 variables cada uno,--dart-definedirecto es suficiente. -
Cero dependencias.
String.fromEnvironmentviene endart:core. No requiere paquetes adicionales ni runtime para leer un.env. -
Compile-time constants.
String.fromEnvironmentse resuelve en tiempo de compilación, no en runtime. El compilador conoce el valor exacto y puede aplicar inlining y dead code elimination. Esto también significa que el valor compilado no puede cambiarse después — no hay riesgo de que un atacante modifique un.enven el dispositivo para apuntar la app a un endpoint diferente. -
Visible en CI con sintaxis estándar. El workflow de CI define las URLs de producción como secretos de GitHub Actions y los inyecta con
--dart-define=KEY=$VALUE. No hay archivos extra que mantener sincronizados. -
Migrable a sistemas más ricos en fases futuras. Si en una fase posterior aparece la necesidad de configuración compleja (dev / qa / staging / prod con secrets distintos, feature flags, A/B testing remoto), se migra a
--dart-define-from-filecon JSON files versionados. La superficie de cambio queda contenida enenv_config.dart: el resto del código sigue consumiendoEnvConfig.apiBaseUrl.
Alternativas evaluadas y descartadas¶
A. --dart-define-from-file con JSON por entorno¶
- Pros: separa la configuración del comando, escala mejor a muchas variables.
- Contras: en MVP solo hay 2 variables; un archivo JSON por entorno es papelería desproporcionada. Los JSON tienen que versionarse o no versionarse; si se versionan, tienen que enmascarar los valores secretos; si no, hay que coordinarlos por canal aparte. Complica el flujo sin beneficio.
- Descartado por: prematuro para MVP. Migración futura preservada.
B. flutter_dotenv (paquete que lee .env en runtime)¶
- Pros: ergonomía tipo Node.js — pone un
.enven la raíz y la app lo lee. - Contras: lectura en runtime significa que el
.envviaja en el bundle de la app y puede ser modificado en el dispositivo. Riesgo de seguridad menor pero real. Además, depende de un paquete de terceros para lo queString.fromEnvironmentya hace nativamente. - Descartado por: lectura runtime + dependencia extra sin beneficio.
C. URLs hardcoded por flavor¶
Separar Android/iOS en flavors (dev, prod) con applicationIdSuffix distinto y URLs hardcoded por flavor.
- Pros: las URLs viven en el build system nativo, no en Dart.
- Contras: duplica la información en
AndroidManifest.xml+Info.plist+build.gradle+Runner.xcodeproj; el código Dart pierde el valor a menos que use plugins de "lectura del flavor"; soluciona el problema para Android/iOS pero no para Web, que no tiene flavors. - Descartado por: fragmenta la configuración por plataforma sin beneficio.
D. Llamada a /.well-known/config.json del backend en arranque¶
La app pide la configuración al backend al arrancar.
- Pros: rota URLs sin recompilar.
- Contras: paradoja del huevo y la gallina: ¿desde qué URL pide la configuración inicial? Pide redirección a esa URL en
String.fromEnvironmentigualmente. Añade una llamada de red en el critical path del splash. - Descartado por: complejidad sin beneficio para los entornos actuales del proyecto.
Implicaciones operativas¶
EnvConfiges la única superficie de acceso a configuración desde el código Dart. Si alguna feature necesita leerapiBaseUrl, lo hace víaEnvConfig.apiBaseUrl, nunca conString.fromEnvironmentdirecto. Esto centraliza el grep cuando un día haga falta refactorizar.- Defaults de localhost: los
defaultValue:apuntan alocalhost:8000ylocalhost:8080.flutter runsin--dart-definearranca contra el stack local. Hacer un build release sin--dart-defineda una app que apunta alocalhost, lo que falla en cualquier dispositivo real — eso es deliberado: una build de release que no especifica URLs no se va a publicar nunca, así que es preferible que falle visiblemente. - Tests no consumen
EnvConfigdirecto: los tests inyectan URLs mock a través de los providers de Riverpod (ADR-012) que envuelvenEnvConfig. La centralización permite override. - No commitear nada con URLs de producción ni secretos. Las URLs de producción son públicas por naturaleza (dominio del proyecto), no son secretos. Pero el patrón debe permanecer estricto: las variables se inyectan en build, no se versionan en el código.
Referencias¶
- Dart —
String.fromEnvironment— documentación oficial. - Flutter — Build modes — flags
--dart-definey--dart-define-from-file. - ADR-004 Cliente HTTP — el cliente lee
EnvConfig.apiBaseUrlpara componer URLs. - ADR-010 OAuth + PKCE + Keycloak — el flujo OIDC lee
EnvConfig.keycloakBaseUrl.