Skip to content

[🐛] RTDB listener provides stale cached data before firing permission denied error; cache not invalidated #8475

Open
@jpancotti

Description

@jpancotti

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:

  1. Upon detecting a permission denial for a path (online), the SDK should trigger the database/permission-denied error.
  2. The SDK should invalidate or mark the cached data for that path as inaccessible.
  3. 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 any value event from the cache for that path, OR
    b. Deliver the value event and immediately deliver a database/permission-denied error again (based on its knowledge that permission was revoked).

Observed Behavior:

  1. A listener is attached to an RTDB path (e.g., /users/{userId}/someData) with offline persistence enabled (firebase.database().setPersistenceEnabled(true)).
  2. Security rules initially grant access (e.g., based on canReadData = true). Data is received correctly via the value callback and stored in the offline cache.
  3. Security rules are updated or the underlying condition changes, denying access (e.g., canReadData becomes false). This happens while the app is online.
  4. Online Permission Denial Sequence:
    a. The listener's value callback often fires first, providing the last known stale data from the offline cache.
    b. Subsequently, the listener's error 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.
  5. 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 a useEffect hook).
    e. The listener's value callback fires, providing the stale data from the offline cache.
    f. Crucially, the error 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.
  6. 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:

  1. When online, the SDK delivers stale cached data via the value listener before signaling the database/permission-denied error.
  2. 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 the value listener but fails to deliver the corresponding database/permission-denied error again. It effectively "forgets" the known permission state when offline.
  3. 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


Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions