Pattern di configurazione degli ambienti che tengono URL, chiavi e feature flag fuori dal codice per web, backend e mobile in dev, staging e prod.

La configurazione hardcoded sembra funzionare il primo giorno. Poi serve un ambiente staging, una seconda API o un rapido switch di una feature, e la modifica “semplice” si trasforma in un rischio per il rilascio. La soluzione è semplice: tieni i valori ambientali fuori dai file sorgente e mettili in una struttura prevedibile.
I soliti problemi sono facili da riconoscere:
“Basta cambiarlo per la prod” crea l'abitudine di modifiche dell'ultimo minuto. Quelle modifiche spesso saltano review, test e ripetibilità. Una persona cambia un URL, un'altra cambia una chiave e ora non riesci a rispondere a una domanda semplice: quale esatta configurazione è stata inclusa con questa build?
Uno scenario comune: costruisci una nuova versione mobile contro lo staging, poi qualcuno cambia l'URL su prod proprio prima del rilascio. Il backend cambia di nuovo il giorno dopo, e devi fare rollback. Se l'URL è hardcoded, il rollback richiede un altro aggiornamento dell'app. Gli utenti aspettano e i ticket di supporto si accumulano.
L'obiettivo qui è uno schema semplice che funzioni su app web, backend Go e app mobile Flutter:
Dev, staging e prod dovrebbero sembrare la stessa app che gira in tre posti diversi. Il punto è cambiare valori, non comportamento.
Devono cambiare tutto ciò che è legato a dove gira l'app o a chi la usa: URL e hostname base, credenziali, integrazioni sandbox vs reali e controlli di sicurezza come livello di logging o impostazioni più severe in prod.
Deve rimanere uguale la logica e il contratto tra le parti. Rotte API, forme di richiesta/risposta, nomi delle feature e regole core di business non dovrebbero variare per ambiente. Se lo staging si comporta diversamente, smette di essere una prova attendibile per la produzione.
Una regola pratica per “nuovo ambiente” vs “nuovo valore di config”: crea un nuovo ambiente solo quando ti serve un sistema isolato (dati separati, accessi e rischi differenti). Se ti servono solo endpoint diversi o numeri diversi, aggiungi un valore di configurazione.
Esempio: vuoi testare un nuovo provider di ricerca. Se è sicuro abilitarlo per un gruppo limitato, mantieni un solo staging e aggiungi un feature flag. Se richiede un database separato e controlli di accesso rigidi, allora ha senso creare un nuovo ambiente.
Una buona configurazione fa una cosa bene: rende difficile spedire per errore un URL di dev, una chiave di test o una feature incompleta.
Usa gli stessi tre livelli per ogni app (web, backend, mobile):
Per evitare confusione, scegli una sola fonte di verità per app e rispettala. Per esempio, il backend legge le variabili d'ambiente all'avvio, l'app web legge variabili a build-time o un piccolo file di config runtime, e l'app mobile legge un piccolo file di ambiente selezionato a build time. La coerenza all'interno di ogni app conta più che forzare lo stesso meccanismo dappertutto.
Uno schema semplice e riutilizzabile:
Dai a ogni voce di configurazione un nome chiaro che risponda a tre domande: cos'è, dove si applica e di che tipo è.
Una convenzione pratica:
Così nessuno deve indovinare se “BASE_URL” è per l'app React, il servizio Go o l'app Flutter.
Il codice React gira nel browser dell'utente, quindi tutto ciò che spedisci è leggibile. L'obiettivo è semplice: tieni i segreti sul server e lascia che il browser legga solo impostazioni “sicure” come API base URL, nome dell'app o un feature toggle non sensibile.
La configurazione a build-time viene iniettata quando costruisci il bundle. Va bene per valori che cambiano di rado e sono sicuri da esporre.
La configurazione runtime viene caricata quando l'app parte (per esempio da un piccolo file JSON servito con l'app o da una globale iniettata). È migliore per valori che potresti voler cambiare dopo il deploy, come cambiare l'API base URL tra ambienti.
Una regola semplice: se cambiarlo non dovrebbe richiedere di ricostruire l'interfaccia, fallo runtime.
Tieni un file locale per gli sviluppatori (non commesso) e imposta i valori reali nella pipeline di deploy.
.env.local (ignorato da git) con qualcosa come VITE_API_BASE_URL=http://localhost:8080VITE_API_BASE_URL come variabile d'ambiente nel job di build, o mettilo in un file di config runtime creato durante il deployEsempio runtime (servito accanto all'app):
{ "apiBaseUrl": "https://api.staging.example.com", "features": { "newCheckout": false } }
Poi caricalo una sola volta all'avvio e tienilo in un unico punto:
export async function loadConfig() {
const res = await fetch('/config.json', { cache: 'no-store' });
return res.json();
}
Tratta tutto ciò che sta nelle env var di React come pubblico. Non mettere password, chiavi API private o URL del database nell'app web.
Esempi sicuri: API base URL, Sentry DSN (pubblico), versione di build e semplici feature flag.
La config del backend resta più sicura quando è tipizzata, caricata dalle variabili d'ambiente e validata prima che il server inizi ad accettare traffico.
Inizia decidendo cosa il backend ha bisogno per girare, e rendi quei valori espliciti. Tipici valori “must have” sono:
APP_ENV (dev, staging, prod)HTTP_ADDR (per esempio :8080)DATABASE_URL (Postgres DSN)PUBLIC_BASE_URL (usato per callback e link)API_KEY (per un servizio di terze parti)Poi caricali in una struct e fallisci presto se manca o è malformato. In questo modo trovi i problemi in secondi, non dopo un deploy parziale.
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
}
Questo mantiene i DSN del database, le chiavi API e gli URL di callback fuori dal codice e fuori da git. In ambienti hostati, inietti queste env var per ambiente così dev, staging e prod possono differire senza cambiare una sola riga.
Le app Flutter di solito necessitano di due livelli di config: flavor a build-time (cosa distribuisci) e impostazioni runtime (cosa l'app può cambiare senza una nuova release). Separarli evita che “solo un rapido cambio URL” diventi una ricostruzione d'emergenza.
Crea tre flavor: dev, staging, prod. I flavor dovrebbero controllare ciò che deve essere fisso a build-time, come nome app, bundle id, firma, progetto analytics e se gli strumenti di debug sono abilitati.
Poi passa solo default non sensibili con --dart-define (o tramite CI) così non li hardcodi mai nel codice:
ENV=stagingDEFAULT_API_BASE=https://api-staging.example.comCONFIG_URL=https://config.example.com/mobile.jsonIn Dart leggili con String.fromEnvironment e costruisci un semplice oggetto AppConfig una sola volta all'avvio.
Se vuoi evitare il rebuild per piccoli cambi di endpoint, non trattare l'API base URL come una costante. Fetch una piccola config all'avvio dell'app (e conservala in cache). Il flavor imposta solo dove recuperare la config.
Una divisione pratica:
Se sposti il backend, aggiorni la remote config per puntare al nuovo base URL. Gli utenti esistenti lo recuperano al prossimo avvio, con un fallback sicuro all'ultimo valore cachato.
I feature flag sono utili per rollout graduali, A/B test, kill switch rapidi e test di cambi rischiosi in staging prima di abilitarli in prod. Non sono un sostituto dei controlli di sicurezza. Se un flag protegge qualcosa che deve essere protetta, non è un flag: è una regola di autorizzazione.
Tratta ogni flag come un'API: nome chiaro, un owner e una data di fine.
Usa nomi che dicono cosa succede quando il flag è ON e quale parte del prodotto tocca. Uno schema semplice:
feature.checkout_new_ui_enabled (feature lato cliente)ops.payments_kill_switch (interruttore d'emergenza)exp.search_rerank_v2 (esperimento)release.api_v3_rollout_pct (rollout graduale)debug.show_network_logs (diagnostica)Preferisci booleani positivi (..._enabled) invece di doppi negativi. Mantieni un prefisso stabile così puoi cercare e fare audit dei flag.
Inizia con default sicuri: se il servizio di flag è giù, l'app dovrebbe comportarsi come la versione stabile.
Un pattern realistico: distribuisci un nuovo endpoint nel backend, tieni il vecchio attivo e usa release.api_v3_rollout_pct per spostare lentamente il traffico. Se gli errori aumentano, torna indietro senza hotfix.
Per evitare l'accumulo di flag, tieni poche regole:
Un “segreto” è tutto ciò che causerebbe danno se divulgato. Pensa a token API, password DB, secret client OAuth, chiavi di firma (JWT), segreti webhook e certificati privati. Non sono segreti: URL base API, numeri di build, feature flag o ID analytics pubblici.
Separa i segreti dal resto delle impostazioni. Gli sviluppatori devono poter cambiare liberamente la config sicura, mentre i segreti vengono iniettati solo a runtime e solo dove necessari.
In dev, tieni i segreti locali e ricreabili. Usa un file .env o il keychain del tuo OS e rendi facile resettarli. Non commetterli mai.
In staging e prod, i segreti dovrebbero stare in uno store dedicato ai segreti, non nel repo, non nelle chat e non incorporati nelle app mobile.
La rotazione fallisce quando si scambia una chiave e si dimentica che client vecchi la stanno ancora usando. Pianifica una finestra di overlap.
Questo approccio di overlap funziona per chiavi API, segreti webhook e chiavi di firma. Evita interruzioni a sorpresa.
Hai un'API di staging e una nuova API di produzione. L'obiettivo è spostare il traffico in fasi, con un modo rapido per tornare indietro se qualcosa non va. Questo è più facile quando l'app legge l'API base URL dalla config, non dal codice.
Tratta l'URL API come un valore di deploy ovunque. Nell'app web (React) è spesso un valore a build-time o un file di config runtime. Nel mobile (Flutter) è tipicamente un flavor più remote config. Nel backend (Go) è una env var a runtime. La parte importante è la coerenza: il codice usa un solo nome di variabile (per esempio API_BASE_URL) e non incorpora mai l'URL in componenti, servizi o schermate.
Un rollout in fasi sicuro può essere così:
La verifica riguarda soprattutto trovare mismatch presto. Prima che veri utenti colpiscano il cambio, conferma che gli endpoint di health rispondono, i flussi di auth funzionano e lo stesso account di test completi un percorso chiave end-to-end.
La maggior parte dei bug di configurazione in produzione è noiosa: un valore di staging rimasto, un default di flag sbagliato o una chiave API mancante in una regione. Un controllo veloce cattura la maggior parte di questi problemi.
Prima di deployare, conferma che tre cose corrispondano all'ambiente target: endpoint, segreti e default.
Poi fai un rapido smoke test. Scegli un flusso reale utente e fallo end to end, usando una installazione fresca o un profilo browser pulito così non fai affidamento su token in cache.
Un'abitudine pratica: tratta lo staging come produzione con valori diversi. Questo significa lo stesso schema di config, le stesse regole di validazione e la stessa forma di deploy. Solo i valori devono differire, non la struttura.
La maggior parte degli outage di configurazione non è esotica. Sono errori semplici che passano perché la config è sparsa tra file, step di build e dashboard e nessuno riesce a rispondere: “Quali valori userà questa app adesso?” Una buona configurazione rende facile rispondere a quella domanda.
Una trappola comune è mettere valori runtime in posti a build-time. Incorpora un API base URL in una build React significa dover ricostruire per ogni ambiente. Poi qualcuno deploya l'artefatto sbagliato e la produzione punta allo staging.
Una regola più sicura: incorpora solo valori che davvero non cambiano dopo il rilascio (come la versione dell'app). Mantieni i dettagli dell'ambiente (API URL, feature switches, endpoint analytics) runtime dove possibile e rendi la fonte di verità ovvia.
Succede quando i default sono “comodi” ma insicuri. Un'app mobile potrebbe defaultare a un'API di dev se non riesce a leggere la config, o un backend potrebbe cadere su un database locale se manca una env var. Questo trasforma un piccolo errore di config in un outage completo.
Due abitudini aiutano:
Un esempio realistico: un rilascio esce venerdì sera e la build di produzione contiene per errore una chiave di pagamento di staging. Tutto “funziona” fino a quando i pagamenti falliscono silenziosamente. La soluzione non è una nuova libreria di pagamento. È la validazione che rifiuta chiavi non-prod in produzione.
Uno staging che non corrisponde alla produzione dà falsa fiducia. Impostazioni database diverse, job in background mancanti o flag extra fanno emergere bug solo dopo il lancio.
Mantieni lo staging vicino alla produzione replicando lo stesso schema di config, le stesse regole di validazione e la stessa forma di deploy. Solo i valori dovrebbero differire, non la struttura.
L'obiettivo non è uno strumento elegante. È la noiosa coerenza: gli stessi nomi, gli stessi tipi, le stesse regole tra dev, staging e prod. Quando la config è prevedibile, i rilasci smettono di essere rischiosi.
Inizia scrivendo un contratto di config chiaro in un solo posto. Mantienilo corto ma specifico: ogni nome di chiave, il suo tipo (stringa, numero, booleano), da dove può provenire (env var, remote config, build-time) e il suo default. Aggiungi note per i valori che non devono mai essere impostati in un client (come chiavi API private). Tratta questo contratto come un'API: le modifiche richiedono review.
Poi fai fallire gli errori presto. Il miglior momento per scoprire un API base URL mancante è in CI, non dopo il deploy. Aggiungi validazione automatizzata che carica la config nello stesso modo in cui la carica la tua app e verifica:
Infine, rendi facile il recupero quando una modifica di config è sbagliata. Fai snapshot di ciò che gira, cambia una cosa alla volta, verifica in fretta e tieni un percorso di rollback.
Se stai buildando e deployando con una piattaforma come Koder.ai (koder.ai), valgono le stesse regole: tratta i valori ambientali come input per build e hosting, tieni i segreti fuori dal codice esportato e valida la config prima di spedire. Quella coerenza è ciò che rende i redeploy e i rollback una routine.
Quando la config è documentata, validata e reversibile, smette di essere una fonte di outage e diventa una parte normale del processo di rilascio.