What you can do with it
- Force an update. Found a bad bug or changed your API? Raise the lowest supported version and older versions show an update screen with a button to the store.
- Suggest an update. Below the latest version, the app can show a gentle “a new version is available” banner instead.
- Maintenance mode. Moving servers or migrating the database? Switch it on and the app shows your message instead of failing requests.
- A notice, once. A new feature, a sale or a survey: each new text gets a new
id, so the app shows it only once. - The app's status. With the
/statuspage on, the file also says whether there's an ongoing outage, so the app can explain instead of showing an error.
The request
Call https://yourapp.apppage.net/app-config.json?platform=android&version=2.3.1 (or platform=ios). With the platform and the app's version, the answer adds update.required (the version is below the lowest supported one) and update.available (below the latest), so the app needs no version comparison of its own. Versions are numbers with dots; a build number after a space, such as 2.3.1 (45), is ignored. Sites on their own domain answer at yourdomain.com/app-config.json.
Add it to your app
Android (Kotlin)
// At launch, off the main thread (uses org.json, built into Android).
suspend fun loadAppConfig(): JSONObject? = withContext(Dispatchers.IO) {
runCatching {
val url = URL("https://yourapp.apppage.net/app-config.json?platform=android&version=${BuildConfig.VERSION_NAME}")
JSONObject(url.readText())
}.getOrNull()
}
lifecycleScope.launch {
val config = loadAppConfig() ?: return@launch // offline: carry on
val maintenance = config.getJSONObject("maintenance")
when {
config.optJSONObject("update")?.optBoolean("required") == true ->
showUpdateScreen(config.getJSONObject("android").optString("storeUrl"), config.optString("updateMessage"))
maintenance.optBoolean("active") -> showMaintenance(maintenance.optString("message"))
config.optJSONObject("notice") != null -> showNoticeOnce(config.getJSONObject("notice")) // remember its "id"
}
}
iOS (Swift)
struct AppConfig: Decodable {
struct Store: Decodable { let storeUrl: String? }
struct Update: Decodable { let required: Bool; let available: Bool }
struct Maintenance: Decodable { let active: Bool; let message: String? }
struct Notice: Decodable { let id: String; let title: String?; let message: String?; let url: String? }
let ios: Store
let update: Update?
let updateMessage: String?
let maintenance: Maintenance
let notice: Notice?
}
func loadAppConfig() async -> AppConfig? {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
guard let url = URL(string: "https://yourapp.apppage.net/app-config.json?platform=ios&version=\(version)"),
let (data, _) = try? await URLSession.shared.data(from: url) else { return nil }
return try? JSONDecoder().decode(AppConfig.self, from: data)
}
// At launch:
if let config = await loadAppConfig() {
if config.update?.required == true { showUpdateScreen(config.ios.storeUrl, config.updateMessage) }
else if config.maintenance.active { showMaintenance(config.maintenance.message) }
else if let notice = config.notice { showNoticeOnce(notice) } // remember notice.id
}
Flutter (Dart)
// pubspec.yaml: http, package_info_plus
Future<Map<String, dynamic>?> loadAppConfig() async {
final info = await PackageInfo.fromPlatform();
final platform = Platform.isIOS ? 'ios' : 'android';
final uri = Uri.parse('https://yourapp.apppage.net/app-config.json?platform=$platform&version=${info.version}');
try {
final res = await http.get(uri).timeout(const Duration(seconds: 5));
return res.statusCode == 200 ? jsonDecode(res.body) as Map<String, dynamic> : null;
} catch (_) {
return null; // offline: carry on
}
}
// At launch:
final config = await loadAppConfig();
if (config?['update']?['required'] == true) {
showUpdateScreen(config![Platform.isIOS ? 'ios' : 'android']['storeUrl'], config['updateMessage']);
} else if (config?['maintenance']?['active'] == true) {
showMaintenance(config!['maintenance']['message']);
}
Good to know
- Offline is fine: if the request fails, let the app carry on. Only an answer from the server should block it.
- Keep the update screen honest: always offer the store button, and use a forced update only when older versions can't work any more.
- Caching: the file is cached for 60 seconds, so a change reaches every app within a minute.
- Web apps can read it too: it allows requests from any origin.
Questions
Do I need Firebase Remote Config?
Not for this. Firebase is great for A/B tests and many keys; for “update, maintenance, notice” a plain JSON file is simpler, has no SDK and works the same on Android, iOS and Flutter.
Is it the same file for Android and iOS?
Yes, with separate versions and store links per platform.