Skip to main content

Remote Config & App Check

Two complementary services. Remote Config lets you change app behavior without shipping a new APK — feature flags, A/B tests, staged rollouts. App Check proves requests come from your real app on real devices, protecting backend APIs from abuse.

Remote Config

What it's for

  • Feature flags — enable a risky feature for 10% of users
  • A/B tests — show variant A vs B, measure conversion
  • Dynamic content — promo banners, onboarding copy
  • Emergency kill switches — disable a broken feature remotely
  • Per-segment config — premium users see different defaults

Setup

implementation("com.google.firebase:firebase-config-ktx")
implementation("com.google.firebase:firebase-analytics-ktx") // for A/B test targeting
@Singleton
class FeatureFlags @Inject constructor() {
private val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig.apply {
setConfigSettingsAsync(remoteConfigSettings {
minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 3600
})

// Local defaults baked into the app — fallback if fetch fails
setDefaultsAsync(R.xml.remote_config_defaults)
}

// Fetch + activate together. Safe to call often; SDK caches.
suspend fun refresh(): Boolean = remoteConfig.fetchAndActivate().await()

// Typed accessors
val newCheckoutEnabled: Boolean get() = remoteConfig.getBoolean("new_checkout_enabled")
val maxUploadMB: Long get() = remoteConfig.getLong("max_upload_mb")
val promoBanner: String? get() = remoteConfig.getString("promo_banner").takeIf { it.isNotBlank() }

val helloVariant: String get() = remoteConfig.getString("hello_variant")

// JSON values
val experimentConfig: ExperimentConfig? get() =
runCatching {
Json.decodeFromString<ExperimentConfig>(remoteConfig.getString("experiment_config"))
}.getOrNull()
}
<!-- res/xml/remote_config_defaults.xml -->
<defaultsMap>
<entry>
<key>new_checkout_enabled</key>
<value>false</value>
</entry>
<entry>
<key>max_upload_mb</key>
<value>10</value>
</entry>
<entry>
<key>promo_banner</key>
<value></value>
</entry>
<entry>
<key>hello_variant</key>
<value>control</value>
</entry>
</defaultsMap>

Fetching strategy

class RemoteConfigInitializer @Inject constructor(
private val flags: FeatureFlags,
private val scope: CoroutineScope
) {
fun install() {
scope.launch {
flags.refresh() // fetch at launch; activate the new values
}

// Periodic refresh while app is active
scope.launch {
while (isActive) {
delay(30.minutes)
flags.refresh()
}
}
}
}

For rollout-critical flags (emergency kill switches), also fetch on cold start before rendering the affected feature.

Realtime Remote Config

Firebase pushes config changes via FCM — no polling needed:

val configUpdateListener = remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
override fun onUpdate(configUpdate: ConfigUpdate) {
remoteConfig.activate().addOnCompleteListener {
// Re-read flag; re-emit from your StateFlow
}
}
override fun onError(error: FirebaseRemoteConfigException) { /* ... */ }
})

// Wrap as Flow
val realtimeFlags: Flow<RemoteConfigSnapshot> = callbackFlow {
val registration = remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
override fun onUpdate(u: ConfigUpdate) {
remoteConfig.activate().addOnCompleteListener {
trySend(RemoteConfigSnapshot.capture(remoteConfig))
}
}
override fun onError(e: FirebaseRemoteConfigException) { }
})
trySend(RemoteConfigSnapshot.capture(remoteConfig))
awaitClose { registration.remove() }
}

Ideal for emergency toggles and config that must propagate within seconds.

A/B testing with Firebase Analytics

// Bucket users via user properties
Firebase.analytics.setUserProperty("experiment_group", "variant_b")

Firebase Console → A/B Testing → create experiment with:

  • Variants (control, variant_a, variant_b)
  • Activation event (first_open, custom event)
  • Goal metrics (retention, engagement, revenue)

Each user lands in a stable bucket (sticky). Results flow back to the console with confidence intervals.

Personalization (automated)

Firebase Personalization uses ML to pick per-user values for numeric or string keys. You define the goal (e.g., maximize retention); Firebase continuously optimizes.

Conditional targeting

In the console, you can set values based on conditions:

  • App version ≥ 2.3.0
  • User property tier = premium
  • Audience (Analytics-defined)
  • Country, language, device model
  • Random percentile (for gradual rollouts)

Ship with new_checkout_enabled = false, then flip to true for 5% of users in a specific country — no APK update.

Using flags in the UI

@HiltViewModel
class CheckoutViewModel @Inject constructor(
private val flags: FeatureFlags
) : ViewModel() {
val uiState: StateFlow<CheckoutUiState> = /* ... */

fun onCheckout() = viewModelScope.launch {
if (flags.newCheckoutEnabled) newCheckoutFlow() else legacyCheckoutFlow()
}
}

@Composable
fun PromoBanner(viewModel: PromoViewModel = hiltViewModel()) {
val banner by viewModel.banner.collectAsStateWithLifecycle()
banner?.let {
Surface(color = MaterialTheme.colorScheme.tertiaryContainer) {
Text(it, Modifier.padding(12.dp))
}
}
}

Flag hygiene

Testing with Remote Config

// In tests, force a specific set of flags
class FakeFeatureFlags : FeatureFlags() {
override val newCheckoutEnabled: Boolean = true
override val maxUploadMB: Long = 50
}

// @BindValue in Hilt tests
@BindValue @JvmField val flags: FeatureFlags = FakeFeatureFlags()

Firebase App Check

What it's for

App Check proves that API requests originate from your legitimate app running on a legitimate device. It mitigates:

  • Scraping (abuse of your Firestore from a script)
  • Credential stuffing (bots brute-forcing sign-in)
  • Quota abuse (exhausting your Firebase budget)
  • Fake installs / emulator abuse (ad fraud)

The protection layers

ProviderWorks onCost
Play IntegrityReal Android devices via PlayFree (quotas apply)
SafetyNet (legacy)Deprecated; migrate to Play Integrity
Debug providerYour dev builds onlyFree
CustomYour own verificationDIY

Most apps: Play Integrity in prod, Debug provider in debug builds.

Setup

implementation("com.google.firebase:firebase-appcheck")
implementation("com.google.firebase:firebase-appcheck-playintegrity")
implementation("com.google.firebase:firebase-appcheck-debug")
class AppCheckInitializer @Inject constructor() {
fun install() {
val appCheck = Firebase.appCheck
if (BuildConfig.DEBUG) {
appCheck.installAppCheckProviderFactory(
DebugAppCheckProviderFactory.getInstance()
)
} else {
appCheck.installAppCheckProviderFactory(
PlayIntegrityAppCheckProviderFactory.getInstance()
)
}
}
}

Debug provider setup

On first debug run, logcat prints a UUID-style token:

D/com.google.firebase.appcheck: Enter this debug secret into the allow list
in the Firebase Console for your project: a1b2c3d4-...

Add that token to Firebase Console → App Check → Manage debug tokens. Now your debug builds can talk to production-protected services.

Enforcing App Check on Firebase services

In Firebase Console → App Check:

  1. Go to the service (Firestore, Storage, Functions, Realtime DB).
  2. Click "Enforce" — requests without a valid App Check token are rejected.

Typically, start in metrics-only mode. Watch for a few days, confirm legitimate traffic is passing, then flip to enforcement.

App Check on your own backend

App Check tokens work beyond Firebase services. Your own backend can verify them:

// Client — get the current token
suspend fun appCheckToken(): String =
Firebase.appCheck.getAppCheckToken(false).await().token

// Interceptor — attach to every request
class AppCheckInterceptor @Inject constructor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = runBlocking { appCheckToken() }
val authed = chain.request().newBuilder()
.header("X-Firebase-AppCheck", token)
.build()
return chain.proceed(authed)
}
}
// Backend (Node) — verify
const {getAppCheck} = require('firebase-admin/app-check');

app.post('/api/private', async (req, res) => {
const token = req.header('X-Firebase-AppCheck');
if (!token) return res.status(401).send('Missing App Check token');

try {
const result = await getAppCheck().verifyToken(token);
// result.appId is the legitimate app
// Continue processing
} catch (e) {
return res.status(401).send('Invalid App Check token');
}
});

App Check token replay & limited use

Tokens are short-lived (~1 hour). For extra-sensitive actions (payment, account deletion), use limited-use tokens — single-use tokens the server verifies and marks consumed:

suspend fun sensitiveAction() {
val token = Firebase.appCheck.getLimitedUseAppCheckToken().await().token
api.deleteAccount(token) // server rejects if token already used
}

Prevents an attacker with a leaked token from replaying it repeatedly.

App Check + Play Integrity API (direct)

If you need more than App Check offers (device integrity verdict, app integrity, licensing), use Play Integrity directly. See Module 16 — Security & Compliance.


Combining the two — a production setup

@Singleton
class FirebaseBootstrap @Inject constructor(
private val appCheckInit: AppCheckInitializer,
private val remoteConfig: FeatureFlags,
private val configInit: RemoteConfigInitializer,
private val channelsInit: NotificationChannelsInitializer
) {
fun install() {
appCheckInit.install() // FIRST — subsequent Firebase calls use the token
channelsInit.create()
configInit.install() // fetch flags in the background
}
}

@HiltAndroidApp
class MyApp : Application() {
@Inject lateinit var firebaseBootstrap: FirebaseBootstrap

override fun onCreate() {
super.onCreate()
firebaseBootstrap.install()
}
}

Common anti-patterns

Anti-patterns

Config / App Check mistakes

  • No local defaults — app breaks if fetch fails
  • Synchronous fetch on main thread blocking startup
  • Flags scattered across codebase (hard to audit)
  • App Check enforced without a metrics-only period
  • Debug token checked into version control
  • Flag values used to enforce security
Best practices

Solid setup

  • Defaults XML + type-safe FeatureFlags class
  • Async refresh with local defaults as fallback
  • One @Singleton FeatureFlags — single source of truth
  • Monitor before enforcing for 3-7 days
  • Debug tokens in .gitignore or added per-dev in Firebase Console
  • Flags for UX/rollout; security is enforced by rules + backend

Key takeaways

Practice exercises

  1. 01

    Feature flag class

    Build a @Singleton FeatureFlags with typed accessors. Back it with XML defaults. Wire into a ViewModel.

  2. 02

    A/B test

    Set up an A/B test in the Firebase console that assigns users to "control" or "variant_a". Gate a small UI change with the variant value.

  3. 03

    Realtime config

    Add addOnConfigUpdateListener. Flip a flag in the console; watch the app update live without restart.

  4. 04

    App Check integration

    Add App Check with Play Integrity in release and Debug provider in debug. Enable metrics-only enforcement on Firestore.

  5. 05

    Backend verification

    Protect your own /api endpoint with X-Firebase-AppCheck header verification using the Admin SDK.

Next

Return to Module 07 Overview or continue to Module 08 — Advanced Components.