[🐛] iOS (New Architecture): Realtime Database set/update/onDisconnect/transaction don't decode __rnfbNull sentinels — nested nulls are written as {__rnfbNull:true}
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- firebase, objective-c, react-native
- Domain
- databases, mobile-dev
Research direction
Start with RNFBDatabaseReferenceHelper.m, RNFBDatabaseOnDisconnectHelper.m, and RNFBDatabaseTransactionHelper.m, comparing their value handling with RNFBStorageHelper.m and RNFBSharedUtils.h. Verify the emulator cases for nested nulls, top-level nulls, priorities, onDisconnect writes, and transactions; done means nulls reach the Firebase iOS SDK as deletions rather than __rnfbNull objects.
Written by the indexing model from the issue text.
Description
Issue
On iOS with the New Architecture (TurboModules), Realtime Database writes that contain a nested null store the null-sentinel object instead of deleting the key.
import { getDatabase, ref, update } from '@react-native-firebase/database';
await update(ref(getDatabase(), 'x'), { a: null, b: 1 });
// iOS: /x/a is stored as { "__rnfbNull": true } instead of being removed
// Android, Web, Admin SDK: /x/a is removed
Behind security rules that validate a (e.g. ".validate": "newData.hasChildren(['foo'])"), the same write fails with permission_denied, because the server sees an object where the app sent a delete. In our app this broke every iOS write that clears a field, such as { isPlaying: false, timer: null }. Writes without nulls worked, so it looked like an intermittent permissions problem.
It affects set (including set(ref, null)), update, setWithPriority, setPriority, onDisconnect().set/setWithPriority/update, and a transaction update function that returns null. We confirmed it with the Realtime Database emulator: a payload containing timer: { __rnfbNull: true } is denied, and the same payload with a real timer: null is allowed.
Root cause
#8774 (the fix for #8144) added null-sentinel encoding for iOS TurboModules:
packages/app/lib/internal/nullSerialization/registry/nativeModuleencode nestednullas{ __rnfbNull: true }wheneverisIOS && isTurboModule.RNFBNullSentinelInterceptor.mdecodes it by swizzling the typedRCTCxxConvert JS_*_Spec*Data:converters.
The Database TurboModule spec types these arguments as a plain Object (e.g. NativeRNFBTurboDatabaseReference.ts: set(..., props: Object), update(..., props: Object)). So no typed converter exists, the interceptor never runs, and the native helpers pass the encoded value straight to the Firebase iOS SDK:
RNFBDatabaseReferenceHelper.m:setValue:[props valueForKey:@"value"],updateChildValues:[props valueForKey:@"values"],setValue:andPriority:,setPriority:RNFBDatabaseOnDisconnectHelper.m:onDisconnectSetValue:,onDisconnectSetValue:andPriority:,onDisconnectUpdateChildValues:RNFBDatabaseTransactionHelper.m:transactionTryCommit,id newValue = [updates valueForKey:@"value"]
Storage already handles the same situation explicitly (RNFBStorageHelper.m calls [RNFBSharedUtils decodeNullSentinels:]). Database doesn't.
Fix
Decode the values with [RNFBSharedUtils decodeNullSentinels:] before they reach the SDK, the same way Storage does. This is the patch we're shipping against 26.3.2. The same code is unchanged in 26.4.0.
// RNFBDatabaseReferenceHelper.m
+#import "RNFBApp/RNFBSharedUtils.h"
- [firDatabaseReference setValue:[props valueForKey:@"value"]
+ [firDatabaseReference setValue:[RNFBSharedUtils decodeNullSentinels:[props valueForKey:@"value"]]
- [firDatabaseReference updateChildValues:[props valueForKey:@"values"]
+ NSDictionary *decodedValues = [RNFBSharedUtils decodeNullSentinels:[props valueForKey:@"values"]];
+ [firDatabaseReference updateChildValues:decodedValues
- [firDatabaseReference setValue:[props valueForKey:@"value"]
- andPriority:[props valueForKey:@"priority"]
+ [firDatabaseReference setValue:[RNFBSharedUtils decodeNullSentinels:[props valueForKey:@"value"]]
+ andPriority:[RNFBSharedUtils decodeNullSentinels:[props valueForKey:@"priority"]]
- [firDatabaseReference setPriority:[props valueForKey:@"priority"]
+ [firDatabaseReference setPriority:[RNFBSharedUtils decodeNullSentinels:[props valueForKey:@"priority"]]
// RNFBDatabaseOnDisconnectHelper.m
+#import "RNFBApp/RNFBSharedUtils.h"
- [firDatabaseReference onDisconnectSetValue:[props valueForKey:@"value"]
+ [firDatabaseReference onDisconnectSetValue:[RNFBSharedUtils decodeNullSentinels:[props valueForKey:@"value"]]
// (same for onDisconnectSetValue:andPriority: — value and priority)
- [firDatabaseReference onDisconnectUpdateChildValues:[props valueForKey:@"values"]
+ NSDictionary *decodedValues = [RNFBSharedUtils decodeNullSentinels:[props valueForKey:@"values"]];
+ [firDatabaseReference onDisconnectUpdateChildValues:decodedValues
// RNFBDatabaseTransactionHelper.m (transactionTryCommit)
- id newValue = [updates valueForKey:@"value"];
+ id newValue = [RNFBSharedUtils decodeNullSentinels:[updates valueForKey:@"value"]];
Notes:
decodeNullSentinels:returnsNSNullfor a top-level sentinel, soset(ref, null)deletes again. It leaves other dictionaries unchanged, soServerValue.TIMESTAMP({".sv":"timestamp"}) passes through intact.- Query modifiers (
RNFBDatabaseQuery.m) aren't affected, because null bounds use the explicitvalueType: "null"path. - Android needs nothing. The encoding is iOS-only.
A more general alternative is to decode in the interceptor for every id/NSDictionary TurboModule argument, not only typed spec structs. That would cover any other module whose spec uses Object params. We're happy to open a PR with the targeted fix above, or with tests, whichever you prefer.
Environment
@react-native-firebase/app/database: 26.3.2 (code unchanged in 26.4.0)- React Native 0.86, New Architecture (TurboModules), Expo SDK 57
- iOS 26 (device, TestFlight release build)
- Android: not affected
- Dominant language
- TypeScript
- Stars
- 12.3k
- Forks
- 2.3k
- Avg merge
- 3d 6h
- Merged PRs (30d)
- 39
Getting set up
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from invertase/react-native-firebase
-
Needs Attention platform: ios plugin: app-core plugin: storage type: bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
invertase/react-native-firebase#9342 · 1 comment ·
Maintainers usually reply within 1 day
-
Needs Attention type: bug
Difficulty 4/5 3-5 days Newbie friendliness 56/100
invertase/react-native-firebase#9348 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
invertase/react-native-firebase#9344 ·
Maintainers usually reply within 1 day
-
blocked: customer-response platform: android plugin: app-check Stale type: bug
Difficulty 4/5 3-5 days Newbie friendliness 42/100
invertase/react-native-firebase#9227 · 7 comments ·
Maintainers usually reply within 1 day
-
blocked: firebase-sdk Keep Open plugin: analytics type: bug
Difficulty 4/5 3-5 days Newbie friendliness 38/100
invertase/react-native-firebase#9115 · 3 comments ·
Maintainers usually reply within 1 day
All issues in invertase/react-native-firebase
Similar issues
-
priority: P2
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
prime-radiant-inc/evener#3291 ·
Maintainers usually reply within 1 day
-
accessibility bug revealjs
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
quarto-dev/quarto-cli#14961 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
supabase/agent-skills#614 ·
-
Content
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
RunestoneInteractive/rs#1559 · 1 comment ·
Maintainers usually reply within 2 days