withConverter.fromFirestore + snapshot.data({ serverTimestamps: 'estimate' }) returns null for pending serverTimestamp() fields
Issue
When a FirestoreDataConverter.fromFirestore callback calls snapshot.data({ serverTimestamps: 'estimate' }), fields written with serverTimestamp() come back as null for any emission where metadata.hasPendingWrites === true — even though 'estimate' is supposed to substitute pending sentinels with a client-clock-estimated Timestamp.
Repro: https://github.com/harelw/rnfb-timestamp-issue-repro
Versions
@react-native-firebase/app: 24.0.0
@react-native-firebase/firestore: 24.0.0
expo: ~55.0.23
react-native: 0.83.6
- iOS simulator (iPhone 17 Pro, iOS 26.x); same shape observed on Android emulator.
- Reproduced against the Firestore emulator (
firebase-tools 14.27.0), so no production project state involved.
Setup
The repo is wired for the Firestore emulator. After pnpm install && pnpm prebuild:
pnpm emulators # terminal 1
pnpm ios # terminal 2
A real GoogleService-Info.plist is required just to clear Firebase iOS SDK's startup validation; all Firestore traffic from the running app goes to localhost:8080.
What the test does
const ref = collection(db, 'rnfb_timestamp_repro').withConverter(sampleConverter);
const docRef = doc(ref); // pre-generate client-side id
await new Promise<void>((resolve, reject) => {
const unsub = onSnapshot(docRef, { includeMetadataChanges: true }, (snap) => {
// snap.data() runs through fromFirestore which calls
// snap.data({ serverTimestamps: 'estimate' })
if (snap.exists() && !snap.metadata.hasPendingWrites) {
unsub();
resolve();
}
}, reject);
// Fire-and-forget — observe local-write echo, then server-acked frame
setDoc(docRef, {}).catch(reject);
});
sampleConverter's toFirestore writes both createdAt and updatedAt as serverTimestamp(). Its fromFirestore reads snap.data({ serverTimestamps: 'estimate' }).
Expected
Per SnapshotOptions.serverTimestamps, 'estimate' should substitute pending serverTimestamps with the client clock. So every emission — including the hasPendingWrites=true ones — should carry Timestamp(<estimated>) for both fields.
Observed
Emissions: 3
[0] exists=true hasPendingWrites=true fromCache=true
createdAt: null
updatedAt: null
[1] exists=true hasPendingWrites=true fromCache=false
createdAt: null
updatedAt: null
[2] exists=true hasPendingWrites=false fromCache=false
createdAt: Timestamp(2026-05-11T19:22:38.776Z)
updatedAt: Timestamp(2026-05-11T19:22:38.776Z)
Both pending emissions return null despite 'estimate' being passed to data(). The settled emission resolves correctly. Two pending emissions, not just one — the bug persists even after the snapshot has been refreshed from network (fromCache=false), as long as hasPendingWrites=true.
Why this matters
Any consumer that validates the converter output (Zod, runtime type assertion, downstream .toMillis()) crashes on the first emission of a freshly-written document. In our app, this surfaced as account-setup failure on first login after sign-up: an immediate read of a doc written with serverTimestamp() failed schema validation because both timestamp fields were null.
The documented 'estimate' workaround is supposed to prevent this exact scenario but doesn't, at least under withConverter.
Current workaround
Inside fromFirestore, check snapshot.metadata.hasPendingWrites and substitute the current Date for known-server-set fields when true. This duplicates what 'estimate' is supposed to do natively, but it's the only way to keep a strict schema in front of the converter today.
withConverter.fromFirestore+snapshot.data({ serverTimestamps: 'estimate' })returnsnullfor pendingserverTimestamp()fieldsIssue
When a
FirestoreDataConverter.fromFirestorecallback callssnapshot.data({ serverTimestamps: 'estimate' }), fields written withserverTimestamp()come back asnullfor any emission wheremetadata.hasPendingWrites === true— even though'estimate'is supposed to substitute pending sentinels with a client-clock-estimated Timestamp.Repro: https://github.com/harelw/rnfb-timestamp-issue-repro
Versions
@react-native-firebase/app:24.0.0@react-native-firebase/firestore:24.0.0expo:~55.0.23react-native:0.83.6firebase-tools14.27.0), so no production project state involved.Setup
The repo is wired for the Firestore emulator. After
pnpm install && pnpm prebuild:A real
GoogleService-Info.plistis required just to clear Firebase iOS SDK's startup validation; all Firestore traffic from the running app goes tolocalhost:8080.What the test does
sampleConverter'stoFirestorewrites bothcreatedAtandupdatedAtasserverTimestamp(). ItsfromFirestorereadssnap.data({ serverTimestamps: 'estimate' }).Expected
Per
SnapshotOptions.serverTimestamps,'estimate'should substitute pending serverTimestamps with the client clock. So every emission — including thehasPendingWrites=trueones — should carryTimestamp(<estimated>)for both fields.Observed
Both pending emissions return
nulldespite'estimate'being passed todata(). The settled emission resolves correctly. Two pending emissions, not just one — the bug persists even after the snapshot has been refreshed from network (fromCache=false), as long ashasPendingWrites=true.Why this matters
Any consumer that validates the converter output (Zod, runtime type assertion, downstream
.toMillis()) crashes on the first emission of a freshly-written document. In our app, this surfaced as account-setup failure on first login after sign-up: an immediate read of a doc written withserverTimestamp()failed schema validation because both timestamp fields werenull.The documented
'estimate'workaround is supposed to prevent this exact scenario but doesn't, at least underwithConverter.Current workaround
Inside
fromFirestore, checksnapshot.metadata.hasPendingWritesand substitute the currentDatefor known-server-set fields when true. This duplicates what'estimate'is supposed to do natively, but it's the only way to keep a strict schema in front of the converter today.