Pattern di configurazione degli ambienti per dev, staging, prod
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.

Perché la configurazione hardcoded continua a creare problemi
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:
- URL base delle API incorporati nell'app (chiamare prod mentre testi, o chiamare dev dopo il rilascio)
- Chiavi API commesse nel repo (leak, fatturazioni a sorpresa, rotazioni d'emergenza)
- Toggle di funzionalità scritti come costanti (devi spedire codice per spegnere qualcosa)
- ID di analytics e report errori hardcoded (i dati finiscono nel posto sbagliato)
“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:
- regole chiare su cosa va nel codice e cosa nella config
- valori predefiniti sicuri per dev, staging e prod
- switch di funzionalità che possano cambiare senza un rebuild completo
- segreti gestiti fuori dal codice, con spazio per ruotarli
Cosa cambia davvero tra dev, staging e prod
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.
Un modello di configurazione pratico riutilizzabile ovunque
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):
- Defaults: valori sicuri che funzionano nella maggior parte dei casi.
- Override per ambiente: cosa cambia per dev, staging, prod.
- Segreti: valori sensibili che non vivono mai nel repo.
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:
- I defaults vivono nel codice come costanti non sensibili (timeout, dimensione pagina, conteggio retry).
- Gli override vivono in file specifici per ambiente o in variabili d'ambiente (API base URL, analytics on/off).
- I segreti vivono in uno secret store e vengono iniettati durante deploy/build (JWT secret, password DB, chiavi di terze parti).
Nomi che la gente capisce
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:
- Prefisso per app: WEB_, API_, MOBILE_
- Usa ALL_CAPS con underscore
- Raggruppa per scopo: API_BASE_URL, AUTH_JWT_SECRET, FEATURES_NEW_CHECKOUT
- Tieni i booleani espliciti: FEATURES_SEARCH_ENABLED=true
Così nessuno deve indovinare se “BASE_URL” è per l'app React, il servizio Go o l'app Flutter.
Passo passo: config per app web (React) senza hardcoding
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.
1) Decidi cosa è build-time vs runtime
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.
2) Conserva l'API base URL senza commetterla
Tieni un file locale per gli sviluppatori (non commesso) e imposta i valori reali nella pipeline di deploy.
- Dev locale: usa
.env.local(ignorato da git) con qualcosa comeVITE_API_BASE_URL=http://localhost:8080 - CI/CD: imposta
VITE_API_BASE_URLcome variabile d'ambiente nel job di build, o mettilo in un file di config runtime creato durante il deploy
Esempio 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();
}
3) Esporre solo valori sicuri al browser
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.
Passo passo: config per backend (Go) che puoi validare
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.
Passo passo: config mobile (Flutter) che resta flessibile
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.
1) Usa i flavors per l'identità, non per gli endpoint
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.json
In Dart leggili con String.fromEnvironment e costruisci un semplice oggetto AppConfig una sola volta all'avvio.
2) Metti URL e switch in una config fetchata
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:
- Flavor (build-time): identità dell'app, URL di default per la config, progetto per crash reporting
- Remote config (runtime): API base URL, feature flag, percentuali di rollout, modalità manutenzione
- Segreti: mai spediti nell'app (i binari mobile possono essere ispezionati)
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.
Feature flag e switch che non diventano caos
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.
Nomi che rendono l'intento ovvio
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.
Defaults, guardrail e pulizia
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:
- Ogni flag ha un owner e una data di “rimozione entro”
- Rimuovi i flag entro 1-2 release dopo il rollout completo
- Logga i valori dei flag nei flussi chiave per il debug
- Revisiona i flag mensilmente come fai con le dipendenze
Segreti: storage, accesso e basi per la rotazione
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.
Dove dovrebbero vivere i segreti (per ambiente)
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.
- Web (React): non mettere segreti nel browser. Se il client ha bisogno di un token, usa un token a vita breve emesso dal backend.
- Backend (Go): carica i segreti da variabili d'ambiente o da un secrets manager all'avvio e tienili solo in memoria.
- Mobile (Flutter): tratta l'app come pubblica. Qualsiasi “segreto” nell'app può essere estratto, quindi usa token emessi dal backend e lo storage sicuro del dispositivo solo per dati di sessione utente.
Basi della rotazione (senza rompere la produzione)
La rotazione fallisce quando si scambia una chiave e si dimentica che client vecchi la stanno ancora usando. Pianifica una finestra di overlap.
- Supporta due segreti validi contemporaneamente (attivo + precedente) per una finestra breve.
- Distribuisci prima il nuovo segreto, poi cambia il puntatore “active”.
- Monitora i fallimenti di auth, poi rimuovi il vecchio segreto dopo la finestra.
- Logga versioni dei segreti (non i valori) per debug sicuro.
Questo approccio di overlap funziona per chiavi API, segreti webhook e chiavi di firma. Evita interruzioni a sorpresa.
Esempio di rollout: cambiare gli URL delle API senza interrompere gli utenti
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ì:
- Distribuisci l'API di prod e tienila “dark” (solo traffico interno) mentre lo staging resta il default.
- Cambia prima le dipendenze del backend (se il backend chiama altri servizi), usando env var e un rapido restart.
- Muovi il traffico web su una piccola fetta (o solo account interni).
- Rilascia l'app mobile con la nuova configurazione, ma mantieni un flag controllato dal server per ritardare lo switch finché sei pronto.
- Aumenta gradualmente il traffico e tieni pronto il rollback.
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.
Checklist rapida prima di spedire
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.
- I base URL puntano al posto giusto (API, auth, CDN, pagamenti). Controlla web, backend e mobile separatamente.
- Nessuna chiave di test in produzione e nessuna chiave di produzione in dev o staging. Conferma anche che i nomi delle chiavi corrispondano a ciò che l'app si aspetta.
- I feature flag hanno default sicuri. Qualsiasi cosa rischiosa dovrebbe essere disabilitata per default e abilitata intenzionalmente.
- Le impostazioni di build e release corrispondono (bundle ID/nome pacchetto, dominio custom, origini CORS, URL di redirect OAuth).
- L'osservabilità è configurata (log, error reporting, tracing) e impostata con l'etichetta di ambiente corretta.
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.
- Apri l'app e conferma che si carica senza errori in console.
- Effettua il login e fai una chiamata API che richiede auth (profilo, impostazioni o una semplice lista dati).
- Forza un errore controllato (input errato o offline) e conferma che appare un messaggio amichevole, non una schermata vuota.
- Controlla log e error reporting: un errore di test dovrebbe apparire sotto l'ambiente corretto entro pochi minuti.
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.
Errori comuni che portano a outage
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.
Mescolare build-time e runtime
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.
Spedire endpoint di dev o chiavi di test
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:
- Fail closed: se manca un valore richiesto, crasha presto con un errore chiaro.
- Rendi la produzione l'ambiente più difficile da configurare male: niente default da dev, niente chiavi di test accettate, nessun endpoint di debug abilitato.
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.
Lasciare lo staging distante dalla 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.
Prossimi passi: rendi la config noiosa, ripetibile e sicura
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:
- i valori richiesti sono presenti (niente stringhe vuote)
- i tipi sono corretti (niente bug "true" vs true)
- le regole solo-per-prod passano (per esempio, HTTPS obbligatorio)
- i feature flag hanno nomi conosciuti (niente typo)
- i segreti non sono stati commessi nel repo
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.
Domande frequenti
Cosa dovrebbe finire nella configurazione invece che nel codice?
Mantieni fuori dai file sorgente i valori che cambiano in base all'ambiente. Inserisci nel codice i valori predefiniti non sensibili, usa impostazioni specifiche dell'ambiente per URL e opzioni e inietta i segreti durante la build o il deploy.
Quando mi serve un nuovo ambiente?
Usa ambienti dev, staging e prod separati quando ti servono dati, accessi o livelli di rischio isolati. Se cambia solo un URL, un limite o un'opzione, aggiungi invece un valore di configurazione.
Posso memorizzare segreti in un'app React?
No. Tutto ciò che viene inviato al browser è visibile agli utenti. Conserva chiavi private e password nel backend ed esponi solo valori sicuri, come un URL API pubblico o un semplice feature flag.
La mia app web dovrebbe usare una configurazione build-time o runtime?
Usa valori di build-time per impostazioni che cambiano raramente e che è sicuro esporre. Carica la configurazione runtime per i valori che potresti dover modificare dopo il deploy, come l'URL di base di un'API o un'opzione di manutenzione.
Come dovrebbe gestire la configurazione un backend Go?
All'avvio del servizio, carica le variabili d'ambiente richieste in una struct di configurazione tipizzata. Convalida gli ambienti consentiti, gli URL e le stringhe di connessione al database, quindi interrompi l'avvio con un errore chiaro se manca un valore obbligatorio.
Come dovrebbe gestire Flutter dev, staging e prod?
Usa i flavor di Flutter per i dettagli specifici della build, come ID dell'app, firma e progetto di analisi. Recupera da una fonte di configurazione remota le impostazioni runtime non sensibili, come URL API e feature flag.
Come posso evitare che i feature flag si accumulino?
Dai ai flag il nome del comportamento che abilitano, ad esempio feature.checkout_new_ui_enabled. Assegna a ogni flag un responsabile e una data di rimozione, imposta su off per default le modifiche rischiose e rimuovi il flag poco dopo il rollout completo.
Dove dovrei memorizzare le chiavi API e le password del database?
Conserva i segreti di staging e produzione in un archivio dedicato, quindi iniettali solo nei servizi che ne hanno bisogno. Mantieni i segreti per lo sviluppo locale in file locali ignorati o nel portachiavi del sistema operativo.
Come posso ruotare i segreti senza causare downtime?
Mantieni insieme le credenziali vecchie e nuove per un breve periodo di sovrapposizione. Distribuisci client o servizi con il nuovo valore, sposta il traffico attivo, controlla gli errori di autenticazione e poi rimuovi la vecchia credenziale.
Cosa dovrei controllare prima di una release in produzione?
Verifica che ogni endpoint punti all'ambiente di destinazione, che i segreti richiesti esistano e che i flag rischiosi siano disattivati per default. Esegui poi un flusso utente completo in un profilo browser pulito o con una nuova installazione dell'app e conferma che i log usino l'etichetta dell'ambiente corretta.