logo

Get in touch

Awesome Image Awesome Image

#Mobile App Development

Implement App Maintenance Mode with Firebase Realtime Database


By Poorvesh Modasiya November 25, 2025

App-Maintenance-Mode-with-Firebase-Realtime-Database

Every serious mobile app needs a clear way to go offline on purpose.When you run database migrations, upgrade backend APIs, or fix a critical bug, you do not want users stuck on spinners, random errors, or half-loaded screens. 

A better pattern is to switch the app into maintenance mode remotely, show a friendly message, and still let your internal team test the app behind the scenes. 

In this guide, we’ll build that pattern using Firebase Realtime Database as a remote control for maintenance mode across Android and iOS. Thanks to its low-latency, real-time sync, Firebase Realtime Database is perfect downtime banners, and simple app-wide kill switches. You can read more about the core concepts in the official Firebase Realtime Database documentation. 

We’ll use the developer implementation you shared as the base, then wrap it in a clean architecture that works in production. 

Why Use Firebase Realtime Database for Maintenance Mode?

You can technically manage maintenance mode by hardcoding flags in the app or exposing a custom backend endpoint, but both approaches are rigid. They usually require a new release or extra infrastructure every time you want to change a message or toggle downtime. That’s exactly where Firebase Realtime Database fits better. 

With Firebase Realtime Database you get benefits like: 

  • Instant updates – any change in the JSON config is synced to all connected clients in milliseconds. 
  • No redeploys – you can toggle maintenance mode or edit messages without publishing a new mobile build. 
  • Simple security rules – Firebase rules and auth let you lock down who can edit maintenance flags. 
  • Cross-platform support – the same config powers Android, iOS, and web apps from a single source of truth. 

Because of this, it’s not just useful for downtime banners. The same pattern works for Firebase Realtime Database examples such as global “app down” screens, per-environment switches (staging vs production), feature toggles, A/B experiments, or even gradual rollouts of new flows all driven from one real-time backend. 

High-Level Objective

Your maintenance mode solution should let admins: 

  • Enable/disable maintenance mode remotely via Firebase Realtime Database 
  • Update titles and messages in real time 
  • Whitelist IPs (internal dev/QA) so the app stays usable for testing while users see downtime 

This pattern fits well into your larger Mobile App Development strategy, especially when you want safer releases and better DX for your teams. 

Firebase Realtime Database Configuration

Create a simple config node that drives all behavior. 

Sample JSON structure:


{ 
  "app_config": { 
    "configureMode": "production", 
    "maintenanceModeStaging": false, 
    "maintenanceModeProduction": true, 
    "message": { 
      "title": "Scheduled Maintenance", 
      "subTitle": "We’ll be back soon!", 
      "description": "Our app is currently undergoing maintenance. Please check back later or contact us at", 
      "hyperLink": "support@helprapp.com" 
    }, 
    "whitelistIps": { 
      "1": "103.120.80.45", 
      "2": "122.175.63.109" 
    } 
  } 
}

This gives you: 

  • Environment-aware toggles: maintenanceModeStaging, maintenanceModeProduction 
  • Message payload: title, subTitle, description, hyperLink 
  • Whitelist: IP addresses that can bypass maintenance mode 
  • You can treat this as a generic real-time feature toggle in Firebase by adding extra flags later (e.g., forceUpdateVersion, disablePayments, etc.).

Android Implementation (Kotlin)

1. Observe app config from Firebase Realtime Database

Your FirebaseRepository should expose a listener for app_config. The core logic in MainActivity (or your root host) looks like this:


private val firebaseRepo = FirebaseRepository() 
 
firebaseRepo.observeAppConfig { config -> 
    config?.let { 
        val ipList = it.whitelistIps?.values?.toList() ?: emptyList() 
 
        val isMaintenanceModeEnabled = if (BuildConfig.DEBUG) { 
            config.maintenanceModeStaging == true 
        } else { 
            config.maintenanceModeProduction == true 
        } 
 
        lifecycleScope.launch(Dispatchers.IO) { 
            val publicIp = Utility.getPublicIp() 
            val isWhitelisted = publicIp?.let { ipList.contains(it) } ?: false 
 
            withContext(Dispatchers.Main) { 
                if (isWhitelisted) { 
                    prefHelper.isMaintenanceMode = false 
                    if (navController.currentDestination?.id == R.id.downtimeFragment) { 
                        navController.navigateUp() 
                    } 
                } else if (isMaintenanceModeEnabled) { 
                    prefHelper.isMaintenanceMode = true 
                    if (navController.currentDestination?.id != R.id.downtimeFragment) { 
                        val bundle = Bundle().apply { putParcelable("appConfig", config) } 
                        navController.navigate( 
                            R.id.action_homeFragment_to_downtimeFragment, 
                            bundle 
                        ) 
                    } 
                } else { 
                    prefHelper.isMaintenanceMode = false 
                    if (navController.currentDestination?.id == R.id.downtimeFragment) { 
                        navController.navigateUp() 
                    } 
                } 
            } 
        } 
    } 
} 

What this does: 

  • Reads the latest config from Firebase Realtime Database 
  • Checks if current build is staging or production 
  • Fetches the device’s public IP and compares with whitelistIps 
  • If IP is not whitelisted and maintenance is ON, navigates user to DowntimeFragment
  • If IP is whitelisted, the app stays fully functional even during downtime

This is a practical firebase realtime database example of using it as a remote feature switch.

2. Navigation Graph: Routing to the downtime screen

In your nav_graph.xml, add an action from your home/root to the downtime screen


<action 

   android:id="@+id/action_homeFragment_to_downtimeFragment" 

   app:destination="@id/downtimeFragment" 

   app:enterAnim="@anim/fade_in" 

   app:exitAnim="@anim/fade_out" 

   app:launchSingleTop="true" 

   app:popEnterAnim="@anim/fade_in" 

   app:popExitAnim="@anim/fade_out" />
 This gives you a single place to control how the UI transitions into maintenance mode. 

3. DowntimeFragment implementation

Use the config from Firebase to populate the UI in real time:


@AndroidEntryPoint 
class DownTimeFragment : BaseFragment() { 
 
    private val args: DownTimeFragmentArgs by navArgs() 
 
    override fun getViewBinding(): FragmentDownTimeBinding = 
        FragmentDownTimeBinding.inflate(layoutInflater) 
 
    override fun setUpUI() { 
        mBinding.apply { 
            val config = args.appConfig 
 
            tvTitle.text = config.message?.title 
            tvSubTitle.text = config.message?.subTitle 
 
            val emailText = context?.spannable( 
                config.message?.hyperLink ?: getString(R.string.hello_helpr_app_com) 
            ) { 
                context?.color(R.color.colorRedText)?.let { color -> textColor = color } 
                clickableSpan = { sendEmail(requireContext(), SUPPORT_EMAIL) } 
            } 
 
            tvDesc.text = TextUtils.concat(config.message?.description, " ", emailText) 
            tvDesc.movementMethod = LinkMovementMethod.getInstance() 
        } 
    } 
} 

This makes your maintenance message fully dynamic: 

  • Change text in Firebase → app updates instantly 
  • Change email link → no redeploy needed 

4. XML Layout for downtime screen


<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:app="http://schemas.android.com/apk/res-auto" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" />
 
    <LinearLayout 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:gravity="center" 
        android:orientation="vertical" 
        app:layout_constraintBottom_toBottomOf="parent" 
        app:layout_constraintTop_toTopOf="parent" 
        app:layout_constraintStart_toStartOf="parent" 
        app:layout_constraintEnd_toEndOf="parent"/> 
 
       <ImageView 
            android:layout_width="80dp" 
            android:layout_height="wrap_content" 
            android:src="@drawable/ic_down_time" />
 
       <TextView 
            android:id="@+id/tvTitle" 
            android:text="Scheduled Maintenance" 
            android:textSize="20sp" 
            android:gravity="center" /> 
 
        <TextView 
            android:id="@+id/tvSubTitle" 
            android:text="This app is currently under maintenance." 
            android:textSize="16sp" 
            android:gravity="center" />  
 
         <TextView 
            android:id="@+id/tvDesc" 
            android:text="We’ll be back soon!" 
            android:textSize="16sp" 
            android:gravity="center" />
  LinearLayout />
androidx.constraintlayout.widget.ConstraintLayout />

You can later expand this with expected return time, icons, or links to support.

Get Free Strategy Call

iOS Implementation (Swift + Firebase Realtime Database)

On iOS, the idea is the same: listen to the same config and show a full-screen maintenance view for non-whitelisted users. 

1. App startup: configure Firebase and observer


In AppDelegate: 

import FirebaseCore 
 
var maintenanceObserver: MaintenanceObserver? 
 
func application( 
    _ application: UIApplication, 
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 
) -> Bool { 
    FirebaseApp.configure() 
    maintenanceObserver = MaintenanceObserver() 
    return true 
}

2. Models and observer


import FirebaseDatabase

struct AppConfig {
    let maintenanceModeProduction: Bool
    let maintenanceModeStaging: Bool
    let message: Message
    let whitelistIps: [String]
}

struct Message {
    let title: String
    let subTitle: String
    let description: String
    let hyperLink: String
}

class MaintenanceObserver {

    private var ref: DatabaseReference!
    private var maintenanceHandle: DatabaseHandle?

    init() {
        ref = Database.database().reference()
        observeAppConfig()
    }

    deinit {
        if let handle = maintenanceHandle {
            ref.removeObserver(withHandle: handle)
        }
    }

    func getPublicIPAddress() -> String {
        var publicIP = ""
        do {
            publicIP = try String(
                contentsOf: URL(string: "https://www.bluewindsolution.com/tools/getpublicip.php")!,
                encoding: .utf8
            ).trimmingCharacters(in: .whitespaces)
        } catch {
            print("Error: \(error)")
        }
        print("publicIP : \(publicIP)")
        return publicIP
    }

    func observeAppConfig() {
        let appConfigRef = ref.child("appConfig")    // or "app_config" based on your schema

        appConfigRef.observe(.value, with: { snapshot in
            guard let dict = snapshot.value as? [String: Any] else { return }

            let maintenanceModeProduction = dict["maintenanceModeProduction"] as? Bool ?? false
            let maintenanceModeStaging = dict["maintenanceModeStaging"] as? Bool ?? false

            var ipList: [String] = []
            if let ipsDict = dict["whitelistIps"] as? [String: Any] {
                ipList = ipsDict.values.compactMap { $0 as? String }
            }

            let messageDict = dict["message"] as? [String: Any] ?? [:]
            let title = messageDict["title"] as? String ?? ""
            let description = messageDict["description"] as? String ?? ""
            let subTitle = messageDict["subTitle"] as? String ?? ""
            let hyperLink = messageDict["hyperLink"] as? String ?? ""

            let message = Message(
                title: title,
                subTitle: subTitle,
                description: description,
                hyperLink: hyperLink
            )

            let appConfig = AppConfig(
                maintenanceModeProduction: maintenanceModeProduction,
                maintenanceModeStaging: maintenanceModeStaging,
                message: message,
                whitelistIps: ipList
            )

            self.handleMaintenanceModeChange(
                isMaintenanceModeProduction: appConfig.maintenanceModeProduction,
                isMaintenanceModeStaging: appConfig.maintenanceModeStaging,
                ipAddresses: appConfig.whitelistIps,
                title: appConfig.message.title,
                subtitle: appConfig.message.subTitle,
                description: appConfig.message.description,
                hyperlink: appConfig.message.hyperLink
            )
        })
    }

    private func handleMaintenanceModeChange(
        isMaintenanceModeProduction: Bool,
        isMaintenanceModeStaging: Bool,
        ipAddresses: [String],
        title: String,
        subtitle: String,
        description: String,
        hyperlink: String
    ) {
        if isMaintenanceModeProduction || isMaintenanceModeStaging {

            let isProd = UIApplication.shared.inferredEnvironment == .appStore
            let isStag = UIApplication.shared.inferredEnvironment != .appStore

            let publicIPAddress = getPublicIPAddress()

            if ipAddresses.contains(publicIPAddress) {
                print("App is live for specific IP address.")
                if let topVC = UIApplication.topViewController() as? MaintainenceViewController {
                    topVC.dismiss(animated: true)
                }
            } else {
                print("App is in maintenance mode.")
                if (isProd && isMaintenanceModeProduction) || (isStag && isMaintenanceModeStaging) {

                    if let topVC = UIApplication.topViewController() as? MaintainenceViewController {
                        topVC.tabBarController?.selectedIndex = 0
                        topVC.dismiss(animated: true) {
                            self.presentMaintenanceVC(
                                title: title,
                                subtitle: subtitle,
                                description: description,
                                hyperlink: hyperlink
                            )
                        }
                    } else {
                        UIApplication.topViewController()?.tabBarController?.selectedIndex = 0
                        self.presentMaintenanceVC(
                            title: title,
                            subtitle: subtitle,
                            description: description,
                            hyperlink: hyperlink
                        )
                    }
                } else {
                    print("App is live.")
                    if let topVC = UIApplication.topViewController() as? MaintainenceViewController {
                        topVC.dismiss(animated: true)
                    }
                }
            }
        } else {
            print("App is live.")
            if let topVC = UIApplication.topViewController() as? MaintainenceViewController {
                topVC.dismiss(animated: true)
            }
        }
    }

    private func presentMaintenanceVC(
        title: String,
        subtitle: String,
        description: String,
        hyperlink: String
    ) {
        let topVC = UIApplication.topViewController()
        let vc = MaintainenceViewController(nibName: "MaintainenceViewController", bundle: nil)
        vc.maintainanceTitle = title
        vc.maintainanceSubtitle = subtitle
        vc.maintainanceDescription = description
        vc.maintainanceHyperlink = hyperlink
        vc.modalPresentationStyle = .fullScreen
        topVC?.present(vc, animated: true, completion: nil)
    }
}

This mirrors the Android behavior:

  • Reads config from Firebase Realtime Database
  • Checks environment
  • Compares device IP against whitelistIps
  • Shows or dismisses a full-screen maintenance controller dynamically

Testing Your Maintenance Toggle

Once Android and iOS are wired:

  1. Set "maintenanceModeProduction": true in Firebase → non-whitelisted users should see the downtime screen.
  2. Add your IP to whitelistIps → app should stay live for your device.
  3. Change the message content → both platforms update instantly without a store release.
  4. Turn all maintenance flags off → app resumes normal flow.

You can also combine this with other patterns from your Firebase Push Notification in JavaScript Apps implementation, for example sending a push notification before you flip maintenance on.

Benefits of This Pattern

  • Real-time control using Firebase Realtime Database
  • Granular access with IP whitelisting for dev/QA
  • No redeploys for message changes or maintenance toggles
  • Better UX during outages, with clear and branded communication
  • Easy to extend into other real-time feature toggles in Firebase

Conclusion

Implementing app-wide downtime handling is not just a “nice to have.” It is part of building reliable, production-grade systems. By pairing Firebase Realtime Database with clean Android and iOS implementations, you get a flexible, real-time maintenance mode that keeps users informed while your team works safely behind the scenes.

If you’re planning to embed this pattern into a larger roadmap feature flags, rollout strategies, or AI-driven experiences it helps to work with an experienced AI Development Agency and Company that understands both backend architecture and mobile delivery. That way, maintenance mode becomes one small, well-designed piece of a much more resilient product.

Free-Strategy -Call

Mobile Application Developer with 6 years of experience building high-quality Android and Flutter apps using Kotlin and Dart. Skilled in scalable architecture (MVVM, Clean Architecture), modern frameworks (Jetpack Compose,GetX, Riverpod), and CI/CD automation. Experienced in delivering secure, user-centric apps across domains like e-commerce, healthcare, fintech, and logistics, and publishing on Play Store & App Store.

Bringing Software Development Expertise to Every
Corner of the World

United States

India

Germany

United Kingdom

Canada

Singapore

Australia

New Zealand

Dubai

Qatar

Kuwait

Finland

Brazil

Netherlands

Ireland

Japan

Kenya

South Africa