Description
Issue 🔥
RTDB listener provides stale cached data offline without error, even after receiving permission denied online
When listening to a Firebase Realtime Database path using @react-native-firebase/database
with offline persistence enabled, if security rules initially allow access but are later changed to deny access, the SDK fails to consistently report the permission denial when the app is subsequently launched offline, leading to stale data being presented as valid.
Expected Behavior:
- Upon detecting a permission denial for a path (online), the SDK should trigger the
database/permission-denied
error. - The SDK should invalidate or mark the cached data for that path as inaccessible.
- If the app is subsequently launched offline and a listener is attached to the same path for which permission was previously denied online, the SDK should either:
a. Not deliver anyvalue
event from the cache for that path, OR
b. Deliver thevalue
event and immediately deliver adatabase/permission-denied
error again (based on its knowledge that permission was revoked).
Observed Behavior:
- A listener is attached to an RTDB path (e.g.,
/users/{userId}/someData
) with offline persistence enabled (firebase.database().setPersistenceEnabled(true)
). - Security rules initially grant access (e.g., based on
canReadData = true
). Data is received correctly via thevalue
callback and stored in the offline cache. - Security rules are updated or the underlying condition changes, denying access (e.g.,
canReadData
becomesfalse
). This happens while the app is online. - Online Permission Denial Sequence:
a. The listener'svalue
callback often fires first, providing the last known stale data from the offline cache.
b. Subsequently, the listener'serror
callback is invoked with the correct[database/permission-denied]
error. The application and SDK now know access is denied for this path.
c. Application-level workarounds (detaching listener, clearing state in the error handler) can mitigate UI display issues in this online scenario. - Critical Problem Sequence (Offline Relaunch After Online Denial):
a. Following the steps above, the app knows permission for/users/{userId}/someData
is denied.
b. The user takes the app offline (disables WiFi/cellular).
c. The user relaunches the app while still offline.
d. The listener for/users/{userId}/someData
is re-attached (e.g., in auseEffect
hook).
e. The listener'svalue
callback fires, providing the stale data from the offline cache.
f. Crucially, theerror
callback with[database/permission-denied]
is NEVER received in this offline relaunch scenario, despite the SDK having previously received this specific error for this specific path while online.
g. The SDK fails to retain or re-apply the known "permission denied" state for the path when operating offline. As a result, the application treats the stale cached data as valid, potentially rendering it, and any error-handling workarounds are never triggered because the necessary error event is missing. - Cache Persistence Issue: This offline behavior strongly suggests the underlying SDK cache for the path is not reliably invalidated or marked as inaccessible even after a
database/permission-denied
error is received online.
Code Snippet (Illustrating Workaround - Ineffective Offline):
// Example from application context/hook
import firebase from '@react-native-firebase/app';
import '@react-native-firebase/database'; // Ensure database module is imported
const someUserId = 'user123'; // Example User ID
const someRef = firebase.database().ref(`/users/${someUserId}/someData`);
let listenerCallback; // Variable to hold the listener function
listenerCallback = someRef.on('value', (snapshot) => {
// !!! PROBLEM 1: This callback fires with STALE CACHED DATA online before error.
// !!! PROBLEM 2: This callback fires with STALE CACHED DATA when relaunching offline,
// !!! even if permission was previously denied online, and NO error follows.
const data = snapshot.val();
console.log(`[Listener] Received potentially stale data for user: ${someUserId}`, data);
// Update application state (might show stale data briefly online, or persistently offline)
// updateStateFunction(data);
}, (error) => {
// !!! PROBLEM 3: This error callback IS RECEIVED online after stale data,
// !!! but IS NOT CALLED AT ALL when relaunching offline after an online denial.
console.error(`[Listener] Error for user: ${someUserId}:`, error);
// Check for the specific permission denied error code
if (error.code === 'database/permission-denied') {
console.log(`[Listener] Permission denied for user: ${someUserId}. Applying workaround.`);
// WORKAROUND (Only effective if error IS received - i.e. online):
// Clear relevant application state immediately
// clearStateFunction();
// Explicitly detach the listener
console.log(`[Listener] Detaching listener for user: ${someUserId}`);
someRef.off('value', listenerCallback);
}
});
// Later in cleanup logic (e.g., useEffect cleanup):
// someRef.off('value', listenerCallback);
Summary of Problem: The core issue is now threefold:
- When online, the SDK delivers stale cached data via the
value
listener before signaling thedatabase/permission-denied
error. - Critically, after receiving a
database/permission-denied
error online for a specific path, if the app is relaunched offline, the SDK delivers stale cached data for that path via thevalue
listener but fails to deliver the correspondingdatabase/permission-denied
error again. It effectively "forgets" the known permission state when offline. - The SDK fails to reliably invalidate the offline cache for a path once permission is known to be denied online.
The application-level workaround (clearing state/detaching listener in the error handler) is completely ineffective for the critical offline relaunch case because the SDK fails to re-signal the previously known error state. This presents a significant data integrity risk for offline-capable apps relying on RTDB permissions.
Project Files
Javascript
Click To Expand
package.json
:
{
"name": "your-app-name", // Generic App Name
"version": "1.5.7", // Example Version
// ... other fields ...
"dependencies": {
"@react-native-async-storage/async-storage": "2.1.2",
"@react-native-clipboard/clipboard": "1.16.2",
"@react-native-community/blur": "4.4.1",
"@react-native-community/netinfo": "11.4.1",
"@react-native-cookies/cookies": "6.2.1",
"@react-native-documents/picker": "^10.1.1",
"@react-native-firebase/analytics": "21.12.3",
"@react-native-firebase/app": "21.12.3",
"@react-native-firebase/auth": "21.12.3",
"@react-native-firebase/database": "21.12.3",
"@react-native-firebase/firestore": "21.12.3",
"@react-native-firebase/functions": "21.12.3",
"@react-native-firebase/messaging": "21.12.3",
"@react-native-firebase/storage": "21.12.3",
"@react-navigation/bottom-tabs": "7.3.3",
"@react-navigation/material-top-tabs": "7.2.3",
"@react-navigation/native": "7.0.19",
"@react-navigation/native-stack": "7.3.3",
"@shopify/flash-list": "1.7.6",
"expo": "52.0.40",
"expo-splash-screen": "^0.29.22",
"expo-updates": "0.27.4",
"moment": "2.30.1",
"react": "18.3.1",
"react-native": "0.77.1",
// ... other dependencies ...
"react-native-maps": "1.20.1",
"react-native-safe-area-context": "^5.3.0",
"react-native-screens": "^4.9.2",
"react-native-svg": "^15.11.2",
"react-native-vector-icons": "10.2.0",
"react-native-webview": "13.13.5"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@react-native/babel-preset": "0.77.1",
"@react-native/eslint-config": "0.77.1",
"@react-native/metro-config": "0.77.1",
"@react-native/typescript-config": "0.77.1",
"eslint": "^9.21.0",
"jest": "^29.7.0",
"prettier": "3.5.3",
"react-test-renderer": "18.3.1",
"typescript": "5.0.4"
// ... other devDependencies ...
}
// ... rest of package.json ...
}
firebase.json
for react-native-firebase v6:
# N/A (Add if you have customizations)
iOS
Click To Expand
ios/Podfile
:
- I'm not using Pods
- I'm using Pods and my Podfile looks like:
ENV['RCT_NEW_ARCH_ENABLED'] = '1' # Set based on your project (New Architecture enabled)
# Resolve react_native_pods.rb with node to allow for hoisting
require Pod::Executable.execute_command('node', ['-p',
'require.resolve(
"react-native/scripts/react_native_pods.rb",
{paths: [process.argv[1]]},
)', __dir__]).strip
# Include Expo autolinking (if using Expo)
# require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
DEPLOYMENT_TARGET = '16.4' # Your iOS deployment target
platform :ios, DEPLOYMENT_TARGET
prepare_react_native_project!
# Firebase configuration
$RNFirebaseAsStaticFramework = true
# Use frameworks for Swift compatibility (needed for Firebase)
use_frameworks! :linkage => :static
# --- PRE_INSTALL HOOK (Example for expo-dev-menu, adjust if needed) ---
# pre_install do |installer|
# installer.pod_targets.each do |pod|
# if pod.name.start_with?('expo-dev-menu') # Adjust condition if necessary
# pod.spec_consumers.each do |consumer|
# if consumer.respond_to?(:pod_target_xcconfig) && consumer.pod_target_xcconfig.is_a?(Hash)
# consumer.pod_target_xcconfig['DEFINES_MODULE'] = 'YES'
# end
# end
# end
# end
# end
target 'YourAppName' do # Replace with your actual target name if different
# use_expo_modules! # Uncomment if using Expo modules
config = use_native_modules!
# Example dependency (Google Maps)
# pod 'react-native-google-maps',
# path: '../node_modules/react-native-maps'
# Firebase RTDB requires leveldb
pod 'leveldb-library', '~> 1.22.2'
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => true, # Set based on your project
:fabric_enabled => ENV['RCT_NEW_ARCH_ENABLED'] == '1', # Set based on your project
:app_path => "#{Pod::Config.instance.installation_root}/.."
# Add other configurations like flipper_configuration if used
)
post_install do |installer|
# Standard React Native post_install steps
react_native_post_install(
installer,
config[:reactNativePath],
mac_catalyst_enabled: false # Set based on your project
)
# Additional post_install steps from your Podfile
installer.pods_project.targets.each do |target|
# ... (Keep existing build phase/settings modifications if necessary) ...
# Example: Ensure Bitcode is disabled and deployment target is set
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = DEPLOYMENT_TARGET
# Firebase specific settings from original Podfile
if target.name.include?("Firebase")
config.build_settings['SWIFT_SUPPRESS_WARNINGS'] = 'YES'
config.build_settings['CLANG_ENABLE_MODULES'] = 'YES'
config.build_settings['DEFINES_MODULE'] = 'YES'
end
# leveldb specific settings from original Podfile
if target.name == "leveldb-library"
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'LEVELDB_PLATFORM_POSIX=1'
end
end
end
end
end
AppDelegate.m
/ AppDelegate.mm
:
// N/A (Add if relevant modifications were made, e.g., Firebase initialization)
Android
Click To Expand
Have you converted to AndroidX?
- my application is an AndroidX application? (Select if correct)
- I am using
android/gradle.settings
jetifier=true
for Android compatibility? (Select if correct) - I am using the NPM package
jetifier
for react-native compatibility? (Select if correct)
android/build.gradle
:
// N/A (Add if relevant modifications were made, e.g., Firebase classpath)
android/app/build.gradle
:
// N/A (Add if relevant modifications were made, e.g., Firebase plugin, dependencies)
android/settings.gradle
:
// N/A (Add if relevant modifications were made)
MainApplication.java
/ MainApplication.kt
:
// N/A (Add if relevant modifications were made)
AndroidManifest.xml
:
<!-- N/A (Add if relevant modifications were made) -->
Environment
Click To Expand
react-native info
output:
info Fetching system and libraries information...
System:
OS: macOS 15.3.2
CPU: (12) arm64 Apple M2 Max
Memory: 136.77 MB / 32.00 GB
Shell: /bin/zsh 5.9
Binaries:
Node: 22.13.1 - /usr/local/bin/node
Yarn: 1.22.22 - /usr/local/bin/yarn
npm: 11.1.0 - /usr/local/bin/npm
Watchman: 2025.04.14.00 - /opt/homebrew/bin/watchman
Managers:
CocoaPods: 1.16.2 - /Users/jo/.rbenv/shims/pod
SDKs:
iOS SDK:
Platforms: DriverKit 24.2, iOS 18.2, macOS 15.2, tvOS 18.2, visionOS 2.2, watchOS 11.2
Android SDK:
API Levels: "33", "34", "35"
Build Tools: 33.0.0, 34.0.0, 35.0.0, 35.0.1, 36.0.0
System Images: android-35 | Google Play ARM 64 v8a
Android NDK: Not Found
IDEs:
Android Studio: 2024.2 AI-242.23726.103.2422.12816248
Xcode: 16.2/16C5032a - /usr/bin/xcodebuild
Languages:
Java: 17.0.14 - /usr/bin/javac
Ruby: 2.7.6 - /Users/jo/.rbenv/shims/ruby
npmPackages:
"@react-native-community/cli": installed: 15.0.1
react: installed: 18.3.1
react-native: installed: 0.77.1
npmGlobalPackages: "*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
- Platform that you're experiencing the issue on: <- SELECT ONE OR MORE (e.g., [x] iOS, [ ] Android)
- [ x ] iOS
- [ x ] Android
- iOS but have not tested behavior on Android
- Android but have not tested behavior on iOS
- Both
react-native-firebase
version you're using that has this issue: 0.77.1@react-native-firebase/app: 21.12.3
,@react-native-firebase/database: 21.12.3
Firebase
module(s) you're using that has the issue:@react-native-firebase/database
- Are you using
TypeScript
?-
Y
&VERSION 5.0.4
-
- 👉 Check out
React Native Firebase
andInvertase
on Twitter for updates on the library.