Home
> Firestore Dates and Timestamps
Get updates on future FREE course and blog posts!
Subscribe

Firestore Dates and Timestamps

13 min read

Jonathan Gamble

jdgamble555 on Wednesday, April 10, 2024 (last modified on Wednesday, April 10, 2024)

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.

  1. A new document is created.
  2. The createdAtTrigger function gets called.
  3. 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.

  1. A new document is created.
  2. The createdAtTrigger function gets called.
  3. The trigger function updates the new document again with the server timestamp field.
  4. 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.

  1. An existing document gets updated.
  2. The updatedAtTrigger function gets called.
  3. The trigger function updates the existing document again with the current timestamp field.
  4. 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.

  1. A document gets updated or created with the serverTimestamp() on a field.
  2. The client cache will get updated immediately for optimistic updates.
  3. 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


Related Posts

© 2024 Code.Build