Storing dates in Cloud Firestore is extremely easy. Convert the date to a Timestamp type, and add it to your JSON. However, storing the current date in a timestamp is a little more complicated. You donât want to store the clientâs date and time, as this could differ from client to client. Always use Firestoreâs server as the baseline.
TL;DR#
The serverTimestamp()
function will allow you to accurately update the current date and time in your Firestore documents. You always want to use the server time, not the client time. You should use this function securely by adding Firestore Rules to ensure the time comes from the server. You could also do this with a Cloud Function, but it will be more expensive and not immediate. Finally, you should configure what happens with optimistic updates when using real-time data to ensure your user has the best experience.
Timestamp#
All dates are stored in Firestore as a Timestamp. To convert the JavaScript dates properly, use Timestamp.fromDate(date: Date)
.
const publishDate = new Date('January 10, 2030');
updateDoc(doc(db, 'posts', ID), {
publishedAt: Timestamp.fromDate(publishDate)
});
Current Date#
When you update a document, you usually want to store the updated date and the created date. The usual two field names for these are updatedAt
and createdAt
. The updated timestamp will reflect the current time the document is updated, while the created timestamp reflects the current time the document is created. Both of these will need to use the current date, but we need to base this on the server time.
Cloud Functions Method#
Using a Firebase Trigger function, we can set the date after a new document has been created or updated. This will be based on the Firebase Functionsâs timestamp, keeping the date on the server, not the client.
â ïž I do not recommend this method for most use cases.
createdAtTrigger#
The first trigger will handle what to do when a document is created. First, we must import our packages and set our PATH
for both functions.
import { FieldValue, Timestamp } from 'firebase-admin/firestore';
import { firestore } from 'firebase-functions';
const PATH = 'todos/{docID}'
Our created trigger function is extremely simple.
exports.createdAtTrigger = firestore
.document(PATH)
.onCreate(async (change) => {
// update createdAt timestamp
change.ref.update({
createdAt: FieldValue.serverTimestamp()
});
});
We could use set
with merge
or an update
function.
exports.createdAtTrigger = firestore
.document(PATH)
.onCreate(async (change) => {
// update createdAt timestamp
change.ref.set({
createdAt: FieldValue.serverTimestamp()
}, { merge: true });
});
Here is how this works.
- A new document is created.
- The
createdAtTrigger
function gets called. - The trigger function updates the new document again with the server timestamp field.
See the problem with this? We have 2 writes and a function call just for one update. This could cost us more money depending on our use case. However, there are technically more steps here, as you will see.
updatedAtTrigger#
Our updated trigger function is more complicated.
exports.updatedAtTrigger = firestore
.document(PATH)
.onUpdate(async (change) => {
const after = change.after.exists ? change.after.data() : null;
const before = change.before.exists ? change.before.data() : null;
// don't update if creating createdAt
if (!before?.createdAt && after?.createdAt) {
return;
}
// don't update if creating updatedAt
if (!before?.updatedAt && after?.updatedAt) {
return;
}
// don't update if updating updatedAt
if (before?.updatedAt && after?.updatedAt) {
if (!(before.updatedAt as Timestamp).isEqual(after.updatedAt)) {
return;
}
}
// update updatedAt timestamp
change.after.ref.update({
updatedAt: FieldValue.serverTimestamp()
});
});
Letâs first relook at the created function once we implement this function. The steps will really look like this.
- A new document is created.
- The
createdAtTrigger
function gets called. - The trigger function updates the new document again with the server timestamp field.
- The
updatedAtTrigger
function gets called. It detects a newly created date, and nothing further happens.
So we are really looking at 2 document writes and 2 function calls for a created timestamp.
Updates, on the other hand, have to be handled prudently. Here is what happens.
- An existing document gets updated.
- The
updatedAtTrigger
function gets called. - The trigger function updates the existing document again with the current timestamp field.
- The
updatedAtTrigger
function gets called again. It detects a newly updated date, and nothing further happens.
You must compare the old and new values to detect a new updated date. When dealing with any Timestamp type in Firestore, you usually need to cast the data using as Timestamp
. This will show you the options on the field you can use. Here we use the isEqual()
property to compare the dates.
Firebase Functions Generation 2#
For the sake of completeness, here are the second-generation counterparts.
import { FieldValue, Timestamp } from 'firebase-admin/firestore';
import {
onDocumentCreated,
onDocumentUpdated
} from 'firebase-functions/v2/firestore';
const PATH = 'todos/{docID}'
exports.createdAtTrigger = onDocumentCreated(PATH, (event) => {
// update createdAt timestamp
event.data?.ref.update({
createdAt: FieldValue.serverTimestamp()
});
});
exports.updatedAtTrigger = onDocumentUpdated(PATH, (event) => {
const after = event.data?.after.exists ? event.data.after.data() : null;
const before = event.data?.before.exists ? event.data.before.data() : null;
// don't update if creating createdAt
if (!before?.createdAt && after?.createdAt) {
return;
}
// don't update if creating updatedAt
if (!before?.updatedAt && after?.updatedAt) {
return;
}
// don't update if updating updatedAt
if (before?.updatedAt && after?.updatedAt) {
if (!(before.updatedAt as Timestamp).isEqual(after.updatedAt)) {
return;
}
}
// update updatedAt timestamp
event.data?.after.ref.update({
updatedAt: FieldValue.serverTimestamp()
});
});
But remember, we should not use these cloud function triggers unless we have a specific use case for them. I honestly donât know of a time when using them is better. If you do, please get in touch with me and let me know.
The Secure Client Version#
The better version is to use server timestamp ()
on your front and secure it. It doesnât require any complex Firebase Functions, and it doesnât need to perform extra reads. It just needs one line of code for each Date type in your Firestore Rules.
Create Timestamp#
Here we use the server timestamp on the client when creating using addDoc
or setDoc
.
addDoc(collection(db, 'posts'), {
title: 'some thing',
createdAt: serverTimestamp()
});
And the server timestamp on the client when updating using updateDoc
or setDoc
with merge
.
updateDoc(doc(db, 'posts', ID), {
title: 'new title rename',
updatedAt: serverTimestamp()
});
And we secure it in Firestore Security Rules.
match /todos/{document} {
allow read;
allow create: if request.time == request.resource.data.createdAt;
allow update: if request.time == request.resource.data.updatedAt;
allow delete;
}
This means the mutation will fail if the createdAt
or updatedAt
dates are not added. We do not get extra function calls or writes. It works out of the box. This is your best bet. I wonât say âalways,â as there may be exceptions. However, I have never seen one. Save money. Use this version.
Real-time Considerations#
When using serverTimestamp()
with real-time data, you must consider what happens when adding or modifying a document.
- A document gets updated or created with the
serverTimestamp()
on a field. - The client cache will get updated immediately for optimistic updates.
- The client cache will then be re-updated with the real server timestamp.
By default, the server timestamp is null. To prevent JavaScript errors, you should set the serverTimestamps
setting when retrieving your client data using onSnapShot
. Your options are estimate | previous | none
. This applies to collections, queries, and individual documents.
const documentData = doc.data({
serverTimestamps: 'estimate'
});
I suggest using an estimate
, as it will be your closest value. For a breakdown, see Data Converters.
Repo: GitHub
Functions File: Index.ts
Timestamp Client: todos.ts
Firestore Rules: firestore.rules
J