Patronen voor omgevingsconfiguratie die URL's, sleutels en featureflags uit code houden voor web, backend en mobiel in dev, staging en prod.

Hardcoded config voelt op dag één prima. Daarna heb je een stagingomgeving, een tweede API of een snelle feature-switch nodig, en die “simpele” wijziging wordt een release-risico. De oplossing is eenvoudig: houd omgevingswaarden uit bronbestanden en zet ze in een voorspelbare opzet.
De gebruikelijke boosdoeners zijn makkelijk te herkennen:
"Verander het gewoon voor prod" creëert de gewoonte van last-minute aanpassingen. Die wijzigingen slaan vaak review, tests en herhaalbaarheid over. De ene persoon verandert een URL, een ander verandert een sleutel, en nu kun je een simpele vraag niet meer beantwoorden: welke exacte config zat bij deze build?
Een veelvoorkomend scenario: je bouwt een nieuwe mobiele versie tegen staging, iemand zet vlak voor release de URL naar prod. De backend verandert de volgende dag weer en je moet terugrollen. Als de URL hardcoded is, betekent terugrollen weer een app-update. Gebruikers wachten, en supporttickets stapelen zich op.
Het doel is een eenvoudig schema dat werkt voor een webapp, een Go-backend en een Flutter-mobiele app:
Dev, staging en prod moeten aanvoelen als dezelfde app die op drie verschillende plekken draait. Het punt is waarden veranderen, niet gedrag.
Wat zou moeten veranderen is alles wat te maken heeft met waar de app draait of wie hem gebruikt: base-URL's en hostnamen, credentials, sandbox- vs echte integraties, en veiligheidsinstellingen zoals logniveau of strengere beveiliging in prod.
Wat hetzelfde moet blijven is de logica en het contract tussen onderdelen. API-routes, request- en response-vormen, feature-namen en kern-businessregels zouden niet per omgeving moeten verschillen. Als staging anders gedraagt, stopt het betrouwbaar oefenen voor productie te zijn.
Een praktische regel voor "nieuwe omgeving" vs "nieuwe configwaarde": maak alleen een nieuwe omgeving aan als je een geïsoleerd systeem nodig hebt (afzonderlijke data, toegang en risico). Als je alleen andere endpoints of andere cijfers nodig hebt, voeg dan een configwaarde toe.
Voorbeeld: je wilt een nieuwe zoekprovider testen. Als het veilig is om het voor een kleine groep in te schakelen, houd één stagingomgeving en voeg een featureflag toe. Als het een aparte database en strikte toegangscontrole vereist, is dat het moment voor een nieuwe omgeving.
Een goede opzet doet één ding goed: het maakt het moeilijk om per ongeluk een dev-URL, testsleutel of onvoltooide feature te deployen.
Gebruik dezelfde drie lagen voor elke app (web, backend, mobiel):
Om verwarring te voorkomen, kies één bron van waarheid per app en houd je daaraan. Bijvoorbeeld: de backend leest bij startup environment-variabelen, de webapp leest buildtime-variabelen of een klein runtime-configbestand, en de mobiele app leest een klein omgevingsbestand dat bij buildtime geselecteerd wordt. Consistentie binnen elke app is belangrijker dan overal precies hetzelfde mechanisme afdwingen.
Een eenvoudig, herbruikbaar schema ziet er zo uit:
Geef elke config-item een duidelijke naam die drie vragen beantwoordt: wat het is, waar het van toepassing is en wat voor type het is.
Een praktische conventie:
Zo hoeft niemand te raden of "BASE_URL" voor de React-app, de Go-service of de Flutter-app is.
React-code draait in de browser van de gebruiker, dus alles wat je meelevert is uit te lezen. Het doel is eenvoudig: houd geheimen op de server, en laat de browser alleen "veilige" instellingen lezen zoals een API-base-URL, app-naam of een niet-gevoelige featureflag.
Build-time-config wordt geïnjecteerd bij het bouwen van de bundle. Dat is prima voor waarden die zelden veranderen en veilig zijn om bloot te stellen.
Runtime-config wordt geladen wanneer de app start (bijvoorbeeld vanuit een klein JSON-bestand dat met de app geserveerd wordt, of een geïnjecteerde global). Het is beter voor waarden die je na deploy wilt kunnen veranderen, zoals het wisselen van een API-base-URL tussen omgevingen.
Een simpele regel: als het veranderen daarvan geen rebuilding van de UI zou moeten vereisen, maak het runtime.
Houd een lokaal bestand voor ontwikkelaars (niet gecommit) en zet echte waarden in je deploy-pijplijn.
.env.local (gitignored) met bijvoorbeeld VITE_API_BASE_URL=http://localhost:8080VITE_API_BASE_URL als environment-variabele in de buildjob, of plaats het in een runtime-configbestand dat tijdens deploy wordt gemaaktRuntime-voorbeeld (geserveerd naast je app):
{ "apiBaseUrl": "https://api.staging.example.com", "features": { "newCheckout": false } }
Laad dat één keer bij startup en bewaar het op één plek:
export async function loadConfig() {
const res = await fetch('/config.json', { cache: 'no-store' });
return res.json();
}
Behandel alles in React env-vars als publiek. Zet er geen wachtwoorden, private API-keys of database-URL's in.
Veilige voorbeelden: API-base-URL, Sentry DSN (publiek), buildversie en simpele featureflags.
Backend-config blijft veiliger wanneer het getypeerd is, geladen wordt vanuit environment-variabelen en gevalideerd wordt voordat de server verkeer accepteert.
Begin met beslissen wat de backend nodig heeft om te draaien, en maak die waarden expliciet. Typische "must have" waarden zijn:
APP_ENV (dev, staging, prod)HTTP_ADDR (bijvoorbeeld :8080)DATABASE_URL (Postgres DSN)PUBLIC_BASE_URL (gebruikt voor callbacks en links)API_KEY (voor een third-party service)Laad ze in een struct en faal snel als iets ontbreekt of malformed is. Zo vind je problemen in seconden, niet na een gedeeltelijke deploy.
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
}
Dit houdt database-DSN's, API-keys en callback-URL's uit code en uit git. In gehoste setups injecteer je deze env-vars per omgeving zodat dev, staging en prod kunnen verschillen zonder één regel te veranderen.
Flutter-apps hebben doorgaans twee configuratielagen nodig: build-time flavors (wat je uitbrengt) en runtime-instellingen (wat de app kan veranderen zonder nieuwe release). Die scheiding voorkomt dat "even snel een URL veranderen" een noodrebuild wordt.
Maak drie flavors: dev, staging, prod. Flavors moeten dingen regelen die bij buildtime vast moeten staan, zoals app-naam, bundle-id, signing, analytics-project en of debugtools ingeschakeld zijn.
Geef alleen niet-gevoelige defaults mee met --dart-define (of je CI) zodat je ze nooit hardcodeert in code:
ENV=stagingDEFAULT_API_BASE=https://api-staging.example.comCONFIG_URL=https://config.example.com/mobile.jsonLees ze in Dart via String.fromEnvironment en bouw een eenvoudige AppConfig-object eenmaal bij startup.
Als je rebuilds wilt vermijden voor kleine endpointwijzigingen, behandel de API-base-URL dan niet als constante. Haal een klein configbestand op bij het opstarten van de app (en cache het). De flavor bepaalt alleen waar het config vandaan komt.
Een praktische splitsing:
Als je je backend verplaatst, update je remote config om naar de nieuwe base-URL te wijzen. Bestaande gebruikers halen dat op bij de volgende launch, met een veilige fallback naar de laatst gecachte waarde.
Featureflags zijn handig voor gefaseerde uitrol, A/B-tests, snel kill-switches en het testen van riskante veranderingen in staging vóór inschakeling in prod. Ze zijn geen vervanging voor beveiligingscontroles. Als een flag iets bewaakt dat beschermd moet worden, is het geen flag—het is een autorisatieregel.
Behandel elke flag als een API: duidelijke naam, een eigenaar en een einddatum.
Gebruik namen die zeggen wat er gebeurt als de flag AAN staat, en welk deel van het product het raakt. Een eenvoudig schema:
feature.checkout_new_ui_enabled (klantgerichte feature)ops.payments_kill_switch (nood-uit)exp.search_rerank_v2 (experiment)release.api_v3_rollout_pct (gefaseerde uitrol)debug.show_network_logs (diagnostiek)Geef de voorkeur aan positieve booleans (..._enabled) boven dubbele negatieven. Houd een stabiele prefix zodat je flags gemakkelijk kunt zoeken en auditen.
Begin met veilige defaults: als de flag-service down is, moet je app zich gedragen als de stabiele versie.
Een realistisch patroon: introduceer een nieuw endpoint in de backend, houd het oude draaiende, en gebruik release.api_v3_rollout_pct om langzaam verkeer te verplaatsen. Als errors stijgen, zet je het terug zonder hotfix.
Om flag-ophoping te voorkomen, hanteer een paar regels:
Een "geheim" is alles dat schade oplevert als het uitlekt. Denk aan API-tokens, databasewachtwoorden, OAuth-clientsecrets, signing keys (JWT), webhook-secrets en private certificaten. Geen geheimen: API-base-URL's, buildnummers, featureflags of publieke analytics-IDs.
Scheid geheimen van de rest van je instellingen. Ontwikkelaars moeten veilige config makkelijk kunnen veranderen, terwijl geheimen alleen bij runtime geïnjecteerd worden en alleen waar nodig.
In dev: houd geheimen lokaal en verwisselbaar. Gebruik een .env-bestand of je OS-keychain en maak het makkelijk te resetten. Commit het nooit.
In staging en prod: plaats geheimen in een dedicated geheimenstore, niet in je code-repo, niet in chatlogs en niet ingebakken in mobiele apps.
Rotatie faalt als je een sleutel verwisselt en vergeet dat oude clients hem nog gebruiken. Plan een overlap-venster.
Deze overlap-aanpak werkt voor API-keys, webhook-secrets en signing keys en voorkomt verrassende uitval.
Je hebt een staging-API en een nieuwe productie-API. Het doel is verkeer gefaseerd over te zetten, met een snelle terugzetmogelijkheid als iets mis lijkt. Dit is makkelijker wanneer de app de API-base-URL uit config leest, niet uit code.
Behandel de API-URL als deploy-time waarde overal. In de webapp (React) is het vaak een build-time waarde of runtime-configbestand. In mobiel (Flutter) is het typisch een flavor plus remote config. In de backend (Go) is het een runtime env-var. Het belangrijkste is consistentie: de code gebruikt één variabelenaam (bijvoorbeeld API_BASE_URL) en embedt nooit URL's in componenten, services of schermen.
Een veilige gefaseerde uitrol kan er zo uitzien:
Verifiëren gaat vooral om mismatches vroeg vangen. Voordat echte gebruikers de wijziging zien, controleer je health endpoints, auth-flows en dat hetzelfde testaccount één cruciale gebruikersreis end-to-end kan doorlopen.
De meeste productie-configfouten zijn saai: een stagingwaarde die is blijven staan, een flagdefault omgeklapt, of een API-key die in één regio ontbreekt. Een snelle controle vangt de meeste.
Controleer vóór deploy of drie dingen overeenkomen met de doelenvironment: endpoints, geheimen en defaults.
Doe daarna een snelle smoke-test. Kies één echte gebruikersflow en doorloop die end-to-end met een verse installatie of een schone browserprofiel zodat je niet op gecachete tokens leunt.
Een praktische gewoonte: behandel staging als productie met andere waarden. Dat betekent hetzelfde config-schema, dezelfde validatieregels en dezelfde deploy-vorm. Alleen de waarden mogen verschillen, niet de structuur.
De meeste configuratie-outages zijn niet exotisch. Het zijn simpele fouten die doorkruipen omdat config over bestanden, buildstappen en dashboards verspreid is en niemand de vraag kan beantwoorden: "Welke waarden zal deze app nu gebruiken?" Een goede opzet maakt die vraag makkelijk te beantwoorden.
Een veelgemaakte valkuil is runtime-waarden in build-time-plaatsen stoppen. Een API-base-URL in een React-build bakken betekent dat je voor elke omgeving opnieuw moet bouwen. Dan wordt er per ongeluk het verkeerde artefact gedeployed en wijst productie naar staging.
Een veiligere regel: bak alleen waarden in die echt nooit veranderen na release (zoals een appversie). Houd omgevingsdetails (API-URL's, feature-switches, analytics-endpoints) runtime waar mogelijk en maak de bron van waarheid duidelijk.
Dit gebeurt als defaults "behulpzaam" maar onveilig zijn. Een mobiele app kan naar een dev-API vallen als hij config niet kan lezen, of een backend kan terugvallen op een lokale database als een env-var mist. Dat verandert een kleine configfout in een volledige outage.
Twee gewoonten helpen:
Een realistisch voorbeeld: een release op vrijdagavond bevat per ongeluk een staging-betalingskey. Alles "werkt" totdat betalingen stilletjes mislukken. De oplossing is geen nieuwe betaalbibliotheek; het is validatie die niet-produktiesleutels in productie afwijst.
Staging die niet overeenkomt met productie geeft valse zekerheid. Verschillende database-instellingen, missende background jobs of extra featureflags zorgen dat bugs pas na lancering zichtbaar worden.
Houd staging dichtbij door hetzelfde config-schema, dezelfde validatieregels en dezelfde deploy-vorm te spiegelen. Alleen de waarden verschillen, niet de structuur.
Het doel is geen fancy tooling. Het is saaie consistentie: dezelfde namen, dezelfde types, dezelfde regels in dev, staging en prod. Als config voorspelbaar is, voelen releases niet meer risicovol.
Begin met het opschrijven van een duidelijk config-contract op één plek. Hou het kort maar specifiek: elke sleutelnaam, het type (string, number, boolean), waar het vandaan mag komen (env var, remote config, build-time) en de default. Voeg notities toe voor waarden die nooit in een client-app mogen staan (zoals private API-keys). Behandel dit contract als een API: wijzigingen hebben review nodig.
Laat fouten vroeg falen. De beste tijd om een missende API-base-URL te ontdekken is in CI, niet na deployment. Voeg geautomatiseerde validatie toe die config op dezelfde manier laadt als je app en controleert:
Maak het ten slotte makkelijk om te herstellen als een configwijziging fout is. Snapshot wat draait, verander één ding tegelijk, verifieer snel en houd een rollback-pad.
Als je bouwt en deployed met een platform zoals Koder.ai (koder.ai), gelden dezelfde regels: behandel omgevingswaarden als inputs voor build en hosting, houd geheimen buiten geëxporteerde source en valideer config voordat je shipt. Die consistentie maakt redeploys en rollbacks routine.
Als config gedocumenteerd, gevalideerd en reversibel is, stopt het een bron van outages te zijn en wordt het een normaal onderdeel van uitrollen.