Extending Firestore with Cloud Functions
With Cloud Functions, you can deploy Node.js code to handle events triggered by changes in your Firestore database. This allows you to easily add server-side functionality into your app without running your own servers.
For examples of use cases, see What Can I Do with Cloud Functions? or the Functions Samples GitHub repository.
Firestore function triggers
The Cloud Functions for Firebase SDK exports a functions.firestore
object that allows you to create handlers tied to specific Firestore events.
Event Type | Trigger |
---|---|
onCreate |
Triggered when a document is written to for the first time. |
onUpdate |
Triggered when a document already exists and has any value changed. |
onDelete |
Triggered when a document with data is deleted. |
onWrite |
Triggered when onCreate , onUpdate or onDelete is triggered. |
If you don't have a project enabled for Cloud Functions for Firebase yet, then read Get Started: Write and Deploy Your First Functions to configure and set up your Cloud Functions for Firebase project.
Writing Firestore-triggered functions
Define a function trigger
To define a Firestore trigger, specify a document path and an event type:
Node.js
const functions = require('firebase-functions');
exports.myFunction = functions.firestore
.document('my-collection/{docId}')
.onWrite((change, context) => { /* ... */ });
Document paths can reference either a specific document or a wildcard pattern.
Specify a single document
If you want to trigger an event for any change to a specific document then you can use the following function.
Node.js
// Listen for any change on document `marie` in collection `users` exports.myFunctionName = functions.firestore .document('users/marie').onWrite((change, context) => { // ... Your code here });
Specify a group of documents using wildcards
If you want to attach a trigger to a group of documents, such as any document in
a certain collection, then use a {wildcard}
in place of the
document ID:
Node.js
// Listen for changes in all documents in the 'users' collection exports.useWildcard = functions.firestore .document('users/{userId}') .onWrite((change, context) => { // If we set `/users/marie` to {name: "Marie"} then // context.params.userId == "marie" // ... and ... // change.after.data() == {name: "Marie"} });
In this example, when any field on any document in users
is changed, it matches
a wildcard called userId
.
If a document in users
has subcollections, and a field in one of those subcollections'
documents is changed, the userId
wildcard is not triggered.
Wildcard matches are extracted from the document path and stored into context.params
.
You may define as many wildcards as you like to substitute explicit collection
or document IDs, for example:
Node.js
// Listen for changes in all documents in the 'users' collection and all subcollections exports.useMultipleWildcards = functions.firestore .document('users/{userId}/{messageCollectionId}/{messageId}') .onWrite((change, context) => { // If we set `/users/marie/incoming_messages/134` to {body: "Hello"} then // context.params.userId == "marie"; // context.params.messageCollectionId == "incoming_messages"; // context.params.messageId == "134"; // ... and ... // change.after.data() == {body: "Hello"} });
Event Triggers
Trigger a function when a new document is created
You can trigger a function to fire any time a new document is created in a collection
by using an onCreate()
handler with a wildcard.
This example function calls createUser
every time a new user profile is added:
Node.js
exports.createUser = functions.firestore .document('users/{userId}') .onCreate((snap, context) => { // Get an object representing the document // e.g. {'name': 'Marie', 'age': 66} const newValue = snap.data(); // access a particular field as you would any JS property const name = newValue.name; // perform desired operations ... });
Trigger a function when a document is updated
You can also trigger a function to fire when a document is updated using the
onUpdate()
function with a wildcard. This example function calls updateUser
if a user
changes their profile:
Node.js
exports.updateUser = functions.firestore .document('users/{userId}') .onUpdate((change, context) => { // Get an object representing the document // e.g. {'name': 'Marie', 'age': 66} const newValue = change.after.data(); // ...or the previous value before this update const previousValue = change.before.data(); // access a particular field as you would any JS property const name = newValue.name; // perform desired operations ... });
Trigger a function when a document is deleted
You can also trigger a function when a document is deleted using the
onDelete()
function with a wildcard. This example
function calls deleteUser
when a user deletes their user profile:
Node.js
exports.deleteUser = functions.firestore .document('users/{userID}') .onDelete((snap, context) => { // Get an object representing the document prior to deletion // e.g. {'name': 'Marie', 'age': 66} const deletedValue = snap.data(); // perform desired operations ... });
Trigger a function for all changes to a document
If you don't care about the type of event being fired, you can listen for all
changes in a Firestore document using the onWrite()
function
with a wildcard. This example function calls modifyUser
if a user is created, updated, or deleted:
Node.js
exports.modifyUser = functions.firestore .document('users/{userID}') .onWrite((change, context) => { // Get an object with the current document value. // If the document does not exist, it has been deleted. const document = change.after.exists ? change.after.data() : null; // Get an object with the previous document value (for update or delete) const oldDocument = change.before.data(); // perform desired operations ... });
Reading and Writing Data
When a function is triggered, it provides a snapshot of the data related to the event. You can use this snapshot to read from or write to the document that triggered the event, or use the Firebase Admin SDK to access other parts of your database.
Event Data
Reading Data
When a function is triggered, you might want to get data from a document that
was updated, or get the data prior to update. You can get the prior data by using
change.before.data()
, which contains the document snapshot before the update.
Similarly, change.after.data()
contains the document snapshot state after the
update.
Node.js
exports.updateUser2 = functions.firestore .document('users/{userId}') .onUpdate((change, context) => { // Get an object representing the current document const newValue = change.after.data(); // ...or the previous value before this update const previousValue = change.before.data(); });
You can access properties as you would in any other object. Alternatively, you
can use the get
function to access specific fields:
Node.js
// Fetch data using standard accessors const age = snap.data().age; const name = snap.data()['name']; // Fetch data using built in accessor const experience = snap.get('experience');
Writing Data
Each function invocation is associated with a specific document in your
Firestore database. You can access that document as a
DocumentReference
in the ref
property of the snapshot returned to your function.
This DocumentReference
comes from the
Firestore Node.js SDK
and includes methods like update()
, set()
, and remove()
so you can easily
modify the document that triggered the function.
Node.js
// Listen for updates to any `user` document. exports.countNameChanges = functions.firestore .document('users/{userId}') .onUpdate((change, context) => { // Retrieve the current and previous value const data = change.after.data(); const previousData = change.before.data(); // We'll only update if the name has changed. // This is crucial to prevent infinite loops. if (data.name == previousData.name) { return null; } // Retrieve the current count of name changes let count = data.name_change_count; if (!count) { count = 0; } // Then return a promise of a set operation to update the count return change.after.ref.set({ name_change_count: count + 1 }, {merge: true}); });
Data outside the trigger event
Cloud Functions execute in a trusted environment, which means they are authorized as a service account on your project. You can perform reads and writes using the Firebase Admin SDK:
Node.js
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
exports.writeToFirestore = functions.firestore
.document('some/doc')
.onWrite((change, context) => {
db.doc('some/otherdoc').set({ ... });
});
Limitations
Note the following limitations for Firestore triggers for Cloud Run functions:
- Cloud Run functions (1st gen) prerequisites an existing "(default)" database in Firestore native mode. It does not support Firestore named databases or Datastore mode. Please use Cloud Run functions (2nd gen) to configure events in such cases.
- Ordering is not guaranteed. Rapid changes can trigger function invocations in an unexpected order.
- Events are delivered at least once, but a single event may result in multiple function invocations. Avoid depending on exactly-once mechanics, and write idempotent functions.
- Firestore in Datastore mode requires Cloud Run functions (2nd gen). Cloud Run functions (1st gen) does not support Datastore mode.
- A trigger is associated with a single database. You cannot create a trigger that matches multiple databases.
- Deleting a database does not automatically delete any triggers for that database. The trigger stops delivering events but continues to exist until you delete the trigger.
- If a matched event exceeds the maximum request size, the
event might not be delivered to Cloud Run functions (1st gen).
- Events not delivered because of request size are logged in platform logs and count towards the log usage for the project.
- You can find these logs in the Logs Explorer with the message "Event cannot deliver to
Cloud function due to size exceeding the limit for 1st gen..." of
error
severity. You can find the function name under thefunctionName
field. If thereceiveTimestamp
field is still within an hour from now, you can infer the actual event content by reading the document in question with a snapshot before and after the timestamp. - To avoid such cadence, you can:
- Migrate and upgrade to Cloud Run functions (2nd gen)
- Downsize the document
- Delete the Cloud Run functions in question
- You can turn off the logging itself using exclusions but note that the offending events will still not be delivered.