We regularly encounter projects where, a month before release, it turns out that a debug API key is going into production. This is not a hypothetical risk — in our practice, there have been about 30 such projects over the past few years. Proper build configuration for Debug, Release, and Staging environments means each environment has its own constants, behavior, and settings, not a set of #if DEBUG scattered throughout the code. For example, one of our clients lost 500,000 rubles because test push notifications were sent to real users. This leak could have been prevented by simple configuration separation. We have been configuring builds for over 5 years and have implemented more than 40 projects — this allows us to guarantee a fast and safe release.
Why Separate Build Configurations?
Classic situation: an application goes to production with a hardcoded https://api-dev.myapp.com base URL. Or testers receive a build that logs everything to the console and crashes due to enabled StrictMode. A Staging configuration reduces the risk of such crashes by 3 times compared to Debug, and Release eliminates them entirely. After configuring builds, these problems disappear forever. Separation of build configurations reduces debugging time by 2 times compared to a monolithic project.
How It Works on iOS
In Xcode, there are two default build configurations: Debug and Release. Staging is added manually: Product → Scheme → Edit Scheme → Duplicate Release → rename to Staging.
For storing configuration values, we use .xcconfig files:
// Config/Debug.xcconfig API_BASE_URL = https://api-dev.myapp.com LOG_LEVEL = verbose BUNDLE_ID_SUFFIX = .debug // Config/Staging.xcconfig API_BASE_URL = https://api-staging.myapp.com LOG_LEVEL = info BUNDLE_ID_SUFFIX = .staging // Config/Release.xcconfig API_BASE_URL = https://api.myapp.com LOG_LEVEL = error BUNDLE_ID_SUFFIX = In Info.plist, values are pulled via $(API_BASE_URL). In code, they are read through Bundle.main.infoDictionary:
enum AppConfig { static var apiBaseURL: URL { guard let urlString = Bundle.main.object(forInfoDictionaryKey: "API_BASE_URL") as? String, let url = URL(string: urlString) else { fatalError("API_BASE_URL not configured") } return url } } No #if DEBUG for URLs — only Bundle.
Using the same scheme, we configure separate app icons and names: add different AppIcon assets and a condition in xcconfig (ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-Staging).
What About Android?
On Android, configurations are managed via build.gradle.kts. Build Types (debug, release, staging) + Product Flavors give a matrix of variants.
android { buildTypes { debug { applicationIdSuffix = ".debug" versionNameSuffix = "-debug" isDebuggable = true buildConfigField("String", "API_BASE_URL", "\"https://api-dev.myapp.com\"") buildConfigField("Boolean", "ENABLE_LOGGING", "true") } create("staging") { initWith(getByName("release")) applicationIdSuffix = ".staging" versionNameSuffix = "-staging" buildConfigField("String", "API_BASE_URL", "\"https://api-staging.myapp.com\"") buildConfigField("Boolean", "ENABLE_LOGGING", "true") signingConfig = signingConfigs.getByName("debug") } release { isMinifyEnabled = true buildConfigField("String", "API_BASE_URL", "\"https://api.myapp.com\"") buildConfigField("Boolean", "ENABLE_LOGGING", "false") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } } BuildConfig.API_BASE_URL is available in code after building — it's a generated class. Note: in Release we enable ProGuard/R8, which reduces APK size by 30-40%. Separate icons and names for Staging and Debug are set via src/debug/res/ and src/staging/res/.
What About Cross-Platform?
In React Native, environment configurations are managed via react-native-config or native build types. The package compiles variables into native code — values are not available via process.env during JS runtime.
In Flutter — --dart-define or --dart-define-from-file:
flutter build apk --dart-define=API_URL=https://api-staging.myapp.com --flavor staging Comparison of Approaches
| Platform | Storage Method | Advantages | Notes |
|---|---|---|---|
| iOS (Swift) | .xcconfig + Info.plist | Clean integration with Xcode | Requires re-scheme build for new environment |
| Android (Kotlin) | buildConfigField / BuildConfig | Auto-generation | BuildConfig persists through ProGuard |
| Flutter | --dart-define / .env | Simplicity | Must explicitly pass during build |
| React Native | react-native-config | Isolation | Additional dependency, ignore .env files |
What the Work Includes and How We Do It
We don't just write configuration files — we analyze the project, find all places with hardcoded and unsafe practices, create a configuration matrix, choose the optimal approach (Xcconfig, buildConfigField, --dart-define), implement, test each configuration, and set up automatic publishing. In the end, you get:
- Audit of current configuration state.
- Creation of xcconfig/buildTypes/productFlavors for all environments.
- Migration of hardcoded values into configuration files.
- Configuration of app icons and names for Staging/Debug.
- Update of CI scripts.
- Documentation for the team.
- Support for 2 weeks after delivery.
How Long Does Configuration Take?
| Project Type | Time | Dependencies |
|---|---|---|
| Single platform (iOS or Android) | 1–2 days | Access to source code and CI system |
| Both platforms | 2–3 days | Access to App Store and Google Play accounts |
| +Flutter or React Native | +1 day | Flutter define or .env files |
The cost is calculated individually. Contact us to get a consultation and accurate estimate for your project.
What Mistakes Does Proper Configuration Prevent?
Proper configuration prevents leakage of confidential data (production keys do not end up in debug builds), confusion with environments (testers always see which version is installed), and failures due to different behavior (on Staging you can enable logging without risk of crashes). After implementing these practices, you will forget about problems related to build configurations. Order configuration setup today — and reduce the risk of financial losses. Get a consultation right now.
ProGuard documentation: https://www.guardsquare.com/manual







