Issue
On iOS, release builds using the Expo config plugin exchange App Check debug tokens against firebaseappcheck.googleapis.com, receiving 403 App attestation failed β and 429 once the per-project Debug Token exchange requests per minute quota (60/min, not adjustable) saturates.
This is the defect reported in discussion #7518 (open since 2023-12-19) and on StackOverflow 77673428. Every report there assumes a race condition. It is not a race β it is deterministic, and it is the line order the config plugin emits.
Android is unaffected.
Root cause
The config plugin injects this into AppDelegate (plugin/src/ios/appDelegate.ts):
RNFBAppCheckModule.sharedInstance()
FirebaseApp.configure()
The resulting sequence:
+[RNFBAppCheckModule sharedInstance] calls [FIRAppCheck setAppCheckProviderFactory:] with a freshly allocated RNFBAppCheckProviderFactory whose providers dictionary is nil (RNFBAppCheckModule.m:36-45).
FirebaseApp.configure() runs [app.container instantiateEagerComponents] (FIRApp.m).
FIRAppCheckInterop is registered with FIRInstantiationTimingAlwaysEager (FIRAppCheckComponent.m), so its creation block runs inside configure().
[[FIRAppCheck alloc] initWithApp:] finds a non-nil factory and calls createProviderWithApp:. With providers[app.name] == nil, the factory installs providerName:@"debug" debugToken:nil (RNFBAppCheckProviderFactory.m:34-39) β FIRAppCheckDebugProvider.
FIRAppCheckDebugProvider with no registered token generates a random one and exchanges it. Hence exchangeDebugToken traffic from release builds.
The line order is the whole mechanism. -[FIRAppCheck initWithApp:] returns nil when no factory is registered β it fails safely and does not crash. Had FirebaseApp.configure() run first, no App Check instance would exist at that point and the JS layer's later configureProvider call would install the correct provider as the first and only one.
No other Firebase product can be the creator on this ordering: a successfully created instance is cached by FIRComponentContainer (*isCacheable = YES), so later consumers (FIRAuth, Messaging, Installations) receive the already-built instance and cannot re-trigger provider selection.
Why it appears intermittent (~20%, not 100%)
The debug provider is installed on every cold start, but an exchange only happens when both hold: the eager auto-refresh reaches the provider before JS configureProvider swaps the delegate, and no live keychain-cached token exists. That timing dependence is why everyone in #7518 concluded "race" β the exchange races, but the misconfiguration is deterministic.
This also explains @brianGammon's finding in #7518 that isTokenAutoRefreshEnabled: false drops the 403 graph to ~0: it removes the eager refresh that reaches the wrongly-defaulted provider.
Android is immune by construction
ReactNativeFirebaseAppCheckProviderFactory.create() throws rather than defaulting to debug. Only iOS silently substitutes a debug provider.
Suggested fix
Make the pre-configure default safe outside DEBUG builds:
#if DEBUG
NSString *defaultProviderName = @"debug";
#else
NSString *defaultProviderName = @"appAttestWithDeviceCheckFallback";
#endif
This fixes the outcome regardless of who calls the factory or in what order, and keeps debug-token development working locally. appAttestWithDeviceCheckFallback already handles the availability fallback in RNFBAppCheckProvider.m.
A complete fix arguably needs both layers β the safe default and corrected ordering in the config plugin so the factory is not registered before FirebaseApp.configure(). I've opened a PR for the factory default only, since changing the plugin's emitted AppDelegate affects every consumer and I can't test that blast radius. Happy to follow up on the plugin if you'd prefer that approach.
The debug default was introduced in ee7df855 ("feat(app-check): add custom factory/provider", 2023-01-19) and is unchanged on main today.
Project Files
Javascript
Click To Expand
package.json:
{
"dependencies": {
"@react-native-firebase/app": "22.3.0",
"@react-native-firebase/app-check": "22.3.0"
}
}
firebase.json for react-native-firebase v6:
# N/A β Expo config plugin used, no firebase.json
iOS
Click To Expand
ios/Podfile:
# Standard Expo-generated Podfile (expo prebuild), no App Check customisation.
AppDelegate.m:
// AppDelegate.swift, generated by `expo prebuild` β plugin-injected block verbatim:
//
// @generated begin @react-native-firebase/app-check - expo prebuild (DO NOT MODIFY)
RNFBAppCheckModule.sharedInstance()
FirebaseApp.configure()
// @generated end @react-native-firebase/app-check
//
// Note the ordering: sharedInstance() registers the provider factory BEFORE
// FirebaseApp.configure() runs its eager components.
Android
Click To Expand
Have you converted to AndroidX?
android/build.gradle:
// N/A β Android is not affected; its factory throws instead of defaulting to debug.
android/app/build.gradle:
android/settings.gradle:
MainApplication.java:
AndroidManifest.xml:
Environment
Click To Expand
react-native info output:
System:
OS: macOS 26.5.2
CPU: (14) arm64 Apple M4 Pro
Memory: 7.30 GB / 48.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 24.16.0
path: ~/.nvm/versions/node/v24.16.0/bin/node
Yarn: Not Found
npm:
version: 8.19.4
path: <project>/node_modules/.bin/npm
Watchman:
version: 2026.07.20.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.17.0
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 25.2
- iOS 26.2
- macOS 26.2
- tvOS 26.2
- visionOS 26.2
- watchOS 26.2
Android SDK:
API Levels:
- "35"
- "36"
- "36"
Build Tools:
- 34.0.0
- 35.0.0
- 36.0.0
- 36.1.0
- 36.1.0
System Images:
- android-35 | Google APIs ARM 64 v8a
- android-36.1 | Google APIs ARM 64 v8a
- android-36.1 | Google Play ARM 64 v8a
- android-36 | Google Play ARM 64 v8a
Android NDK: Not Found
IDEs:
Android Studio: 2025.2 AI-252.28238.7.2523.14688667
Xcode:
version: 26.2/17C52
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.20
path: /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/javac
Ruby:
version: 4.0.6
path: /opt/homebrew/bin/ruby
npmPackages:
"@react-native-community/cli":
installed: 20.0.0
wanted: 20.0.0
react:
installed: 19.1.0
wanted: 19.1.0
react-native:
installed: 0.81.5
wanted: 0.81.5
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: Not found
newArchEnabled: Not found
iOS:
hermesEnabled: Not found
newArchEnabled: Not found
info React Native v0.86.2 is now available (your project is running on v0.81.5).
info Changelog: https://github.com/facebook/react-native/releases/tag/v0.86.2
info Diff: https://react-native-community.github.io/upgrade-helper/?from=0.81.5&to=0.86.2
info For more info, check out "https://reactnative.dev/docs/upgrading?os=macos".
- Platform that you're experiencing the issue on:
react-native-firebase version you're using that has this issue:
22.3.0 (also present on main β the file is byte-identical)
Firebase module(s) you're using that has the issue:
- Are you using
TypeScript?
Issue
On iOS, release builds using the Expo config plugin exchange App Check debug tokens against
firebaseappcheck.googleapis.com, receiving403 App attestation failedβ and429once the per-projectDebug Token exchange requests per minutequota (60/min, not adjustable) saturates.This is the defect reported in discussion #7518 (open since 2023-12-19) and on StackOverflow 77673428. Every report there assumes a race condition. It is not a race β it is deterministic, and it is the line order the config plugin emits.
Android is unaffected.
Root cause
The config plugin injects this into
AppDelegate(plugin/src/ios/appDelegate.ts):The resulting sequence:
+[RNFBAppCheckModule sharedInstance]calls[FIRAppCheck setAppCheckProviderFactory:]with a freshly allocatedRNFBAppCheckProviderFactorywhoseprovidersdictionary is nil (RNFBAppCheckModule.m:36-45).FirebaseApp.configure()runs[app.container instantiateEagerComponents](FIRApp.m).FIRAppCheckInteropis registered withFIRInstantiationTimingAlwaysEager(FIRAppCheckComponent.m), so its creation block runs insideconfigure().[[FIRAppCheck alloc] initWithApp:]finds a non-nil factory and callscreateProviderWithApp:. Withproviders[app.name] == nil, the factory installsproviderName:@"debug" debugToken:nil(RNFBAppCheckProviderFactory.m:34-39) βFIRAppCheckDebugProvider.FIRAppCheckDebugProviderwith no registered token generates a random one and exchanges it. HenceexchangeDebugTokentraffic from release builds.The line order is the whole mechanism.
-[FIRAppCheck initWithApp:]returns nil when no factory is registered β it fails safely and does not crash. HadFirebaseApp.configure()run first, no App Check instance would exist at that point and the JS layer's laterconfigureProvidercall would install the correct provider as the first and only one.No other Firebase product can be the creator on this ordering: a successfully created instance is cached by
FIRComponentContainer(*isCacheable = YES), so later consumers (FIRAuth, Messaging, Installations) receive the already-built instance and cannot re-trigger provider selection.Why it appears intermittent (~20%, not 100%)
The debug provider is installed on every cold start, but an exchange only happens when both hold: the eager auto-refresh reaches the provider before JS
configureProviderswaps the delegate, and no live keychain-cached token exists. That timing dependence is why everyone in #7518 concluded "race" β the exchange races, but the misconfiguration is deterministic.This also explains @brianGammon's finding in #7518 that
isTokenAutoRefreshEnabled: falsedrops the 403 graph to ~0: it removes the eager refresh that reaches the wrongly-defaulted provider.Android is immune by construction
ReactNativeFirebaseAppCheckProviderFactory.create()throws rather than defaulting to debug. Only iOS silently substitutes a debug provider.Suggested fix
Make the pre-configure default safe outside
DEBUGbuilds:This fixes the outcome regardless of who calls the factory or in what order, and keeps debug-token development working locally.
appAttestWithDeviceCheckFallbackalready handles the availability fallback inRNFBAppCheckProvider.m.A complete fix arguably needs both layers β the safe default and corrected ordering in the config plugin so the factory is not registered before
FirebaseApp.configure(). I've opened a PR for the factory default only, since changing the plugin's emittedAppDelegateaffects every consumer and I can't test that blast radius. Happy to follow up on the plugin if you'd prefer that approach.The debug default was introduced in
ee7df855("feat(app-check): add custom factory/provider", 2023-01-19) and is unchanged onmaintoday.Project Files
Javascript
Click To Expand
package.json:{ "dependencies": { "@react-native-firebase/app": "22.3.0", "@react-native-firebase/app-check": "22.3.0" } }firebase.jsonfor react-native-firebase v6:# N/A β Expo config plugin used, no firebase.jsoniOS
Click To Expand
ios/Podfile:# Standard Expo-generated Podfile (expo prebuild), no App Check customisation.AppDelegate.m:Android
Click To Expand
Have you converted to AndroidX?
android/gradle.settingsjetifier=truefor Android compatibility?jetifierfor react-native compatibility?android/build.gradle:// N/A β Android is not affected; its factory throws instead of defaulting to debug.android/app/build.gradle:// N/Aandroid/settings.gradle:// N/AMainApplication.java:// N/AAndroidManifest.xml:<!-- N/A -->Environment
Click To Expand
react-native infooutput:react-native-firebaseversion you're using that has this issue:22.3.0(also present onmainβ the file is byte-identical)Firebasemodule(s) you're using that has the issue:App CheckTypeScript?Y&5.9.3